Skip to content

Commit e222f02

Browse files
fix(rpc agent): handle properly cross rpc relations (#220)
1 parent 75f0c87 commit e222f02

2 files changed

Lines changed: 114 additions & 6 deletions

File tree

packages/rpc-agent/src/agent.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import type { RpcSchema } from './types';
33
import { Agent, AgentOptions } from '@forestadmin/agent';
44
import { ForestAdminHttpDriverServices } from '@forestadmin/agent/dist/services';
55
import { DataSourceOptions, TCollectionName, TSchema } from '@forestadmin/datasource-customizer';
6-
import { Collection, DataSource, DataSourceFactory } from '@forestadmin/datasource-toolkit';
6+
import {
7+
Collection,
8+
DataSource,
9+
DataSourceFactory,
10+
RelationSchema,
11+
} from '@forestadmin/datasource-toolkit';
712
import { createHash } from 'crypto';
813
import fs from 'fs/promises';
914

@@ -122,23 +127,23 @@ export default class RpcAgent<S extends TSchema = TSchema> extends Agent<S> {
122127
}
123128

124129
buildSchema(dataSource: DataSource): RpcSchema {
125-
const rpcRelations = {};
126-
const collections = [];
130+
const rpcRelations: RpcSchema['rpc_relations'] = {};
131+
const collections: RpcSchema['collections'] = [];
127132

128133
dataSource.collections.forEach(collection => {
129-
const relations = {};
134+
const relations: Record<string, RelationSchema> = {};
130135

131136
if (this.rpcCollections.includes(collection.name)) {
132137
Object.entries(collection.schema.fields).forEach(([name, field]) => {
133138
if (field.type !== 'Column' && !this.rpcCollections.includes(field.foreignCollection)) {
134139
relations[name] = keysToSnake(field);
135140
}
136141
});
137-
138-
if (Object.keys(relations).length > 0) rpcRelations[collection.name] = relations;
139142
} else {
140143
collections.push(this.buildCollection(collection, relations));
141144
}
145+
146+
if (Object.keys(relations).length > 0) rpcRelations[collection.name] = relations;
142147
});
143148

144149
return {

packages/rpc-agent/test/agent.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@ function buildCollectionFixture(actionSchema: Record<string, unknown>) {
2323
} as any;
2424
}
2525

26+
function buildColumn(operators: string[] = ['Equal']) {
27+
return { type: 'Column', filterOperators: new Set(operators) };
28+
}
29+
30+
function buildRelation(foreignCollection: string) {
31+
return { type: 'ManyToOne', foreignCollection, foreignKey: `${foreignCollection}Id` };
32+
}
33+
34+
function buildSchemaCollection(name: string, fields: Record<string, unknown>) {
35+
return {
36+
name,
37+
schema: {
38+
fields,
39+
actions: {},
40+
aggregationCapabilities: {
41+
supportedDateOperations: new Set<string>(),
42+
supportGroups: false,
43+
},
44+
countable: false,
45+
searchable: false,
46+
charts: [],
47+
segments: [],
48+
},
49+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50+
} as any;
51+
}
52+
2653
describe('RpcAgent.buildCollection', () => {
2754
it('serialises generateFile as is_generate_file so Ruby main agents read it natively', () => {
2855
const agent = createAgent();
@@ -42,3 +69,79 @@ describe('RpcAgent.buildCollection', () => {
4269
});
4370
});
4471
});
72+
73+
describe('RpcAgent.buildSchema', () => {
74+
function createAgentWithRpcCollections(rpcCollections: string[]): RpcAgent {
75+
const agent = createAgent();
76+
(agent as unknown as { rpcCollections: string[] }).rpcCollections = rpcCollections;
77+
78+
return agent;
79+
}
80+
81+
function buildDataSource(collections: unknown[]) {
82+
return {
83+
collections,
84+
schema: { charts: [] },
85+
nativeQueryConnections: {},
86+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
87+
} as any;
88+
}
89+
90+
// Non-regression: a NON-RPC collection holding a relation toward an RPC collection
91+
// must be exposed in `rpc_relations`. The cross relation used to be computed in
92+
// buildCollection then dropped because the registration lived inside the RPC branch only.
93+
it('exposes cross relations from a non-RPC collection pointing to an RPC collection', () => {
94+
const agent = createAgentWithRpcCollections(['Books']);
95+
96+
// Authors is a main (non-RPC) collection referencing the RPC collection Books.
97+
const authors = buildSchemaCollection('Authors', {
98+
id: buildColumn(),
99+
myBook: buildRelation('Books'),
100+
});
101+
const books = buildSchemaCollection('Books', { id: buildColumn() });
102+
103+
const schema = agent.buildSchema(buildDataSource([authors, books]));
104+
105+
expect(schema.rpc_relations.Authors).toBeDefined();
106+
expect(schema.rpc_relations.Authors.myBook).toMatchObject({
107+
type: 'ManyToOne',
108+
foreign_collection: 'Books',
109+
});
110+
111+
// The cross relation is stripped from the exposed collection fields.
112+
const exposedAuthors = schema.collections.find(c => c.name === 'Authors');
113+
expect(exposedAuthors.fields).toHaveProperty('id');
114+
expect(exposedAuthors.fields).not.toHaveProperty('myBook');
115+
});
116+
117+
it('keeps exposing relations from an RPC collection toward non-RPC collections', () => {
118+
const agent = createAgentWithRpcCollections(['Books']);
119+
120+
const books = buildSchemaCollection('Books', {
121+
id: buildColumn(),
122+
author: buildRelation('Authors'),
123+
});
124+
const authors = buildSchemaCollection('Authors', { id: buildColumn() });
125+
126+
const schema = agent.buildSchema(buildDataSource([books, authors]));
127+
128+
expect(schema.rpc_relations.Books).toBeDefined();
129+
expect(schema.rpc_relations.Books.author).toMatchObject({
130+
type: 'ManyToOne',
131+
foreign_collection: 'Authors',
132+
});
133+
// The RPC collection itself is not added to the exposed `collections` array.
134+
expect(schema.collections.find(c => c.name === 'Books')).toBeUndefined();
135+
});
136+
137+
it('omits collections without cross relations from rpc_relations', () => {
138+
const agent = createAgentWithRpcCollections(['Books']);
139+
140+
const books = buildSchemaCollection('Books', { id: buildColumn() });
141+
const authors = buildSchemaCollection('Authors', { id: buildColumn() });
142+
143+
const schema = agent.buildSchema(buildDataSource([books, authors]));
144+
145+
expect(schema.rpc_relations).toEqual({});
146+
});
147+
});

0 commit comments

Comments
 (0)