@objectstack/objectql 3.3.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +25 -0
- package/dist/index.d.mts +27 -17
- package/dist/index.d.ts +27 -17
- package/dist/index.js +221 -109
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +221 -109
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/engine.test.ts +13 -13
- package/src/engine.ts +36 -77
- package/src/plugin.ts +2 -2
- package/src/protocol-data.test.ts +41 -38
- package/src/protocol-discovery.test.ts +25 -25
- package/src/protocol-meta.test.ts +440 -0
- package/src/protocol.ts +258 -68
- package/tsconfig.json +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/objectql",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Isomorphic ObjectQL Engine for ObjectStack",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@objectstack/core": "
|
|
17
|
-
"@objectstack/spec": "
|
|
18
|
-
"@objectstack/types": "
|
|
16
|
+
"@objectstack/core": "4.0.0",
|
|
17
|
+
"@objectstack/spec": "4.0.0",
|
|
18
|
+
"@objectstack/types": "4.0.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"typescript": "^
|
|
22
|
-
"vitest": "^4.1.
|
|
21
|
+
"typescript": "^6.0.2",
|
|
22
|
+
"vitest": "^4.1.2"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsup --config ../../tsup.config.ts",
|
package/src/engine.test.ts
CHANGED
|
@@ -248,7 +248,7 @@ describe('ObjectQL Engine', () => {
|
|
|
248
248
|
{ id: 'u2', name: 'Bob' },
|
|
249
249
|
]);
|
|
250
250
|
|
|
251
|
-
const result = await engine.find('task', {
|
|
251
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
252
252
|
|
|
253
253
|
expect(result).toHaveLength(2);
|
|
254
254
|
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
@@ -288,7 +288,7 @@ describe('ObjectQL Engine', () => {
|
|
|
288
288
|
{ id: 'o1', total: 100 },
|
|
289
289
|
]);
|
|
290
290
|
|
|
291
|
-
const result = await engine.find('order_item', {
|
|
291
|
+
const result = await engine.find('order_item', { expand: { order: { object: 'order' } } });
|
|
292
292
|
expect(result[0].order).toEqual({ id: 'o1', total: 100 });
|
|
293
293
|
});
|
|
294
294
|
|
|
@@ -304,7 +304,7 @@ describe('ObjectQL Engine', () => {
|
|
|
304
304
|
{ id: 't1', title: 'Task 1' },
|
|
305
305
|
]);
|
|
306
306
|
|
|
307
|
-
const result = await engine.find('task', {
|
|
307
|
+
const result = await engine.find('task', { expand: { title: { object: 'title' } } });
|
|
308
308
|
expect(result[0].title).toBe('Task 1'); // Unchanged
|
|
309
309
|
expect(mockDriver.find).toHaveBeenCalledTimes(1); // No expand query
|
|
310
310
|
});
|
|
@@ -316,7 +316,7 @@ describe('ObjectQL Engine', () => {
|
|
|
316
316
|
{ id: 't1', assignee: 'u1' },
|
|
317
317
|
]);
|
|
318
318
|
|
|
319
|
-
const result = await engine.find('task', {
|
|
319
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
320
320
|
expect(result[0].assignee).toBe('u1'); // Unchanged — raw ID
|
|
321
321
|
expect(mockDriver.find).toHaveBeenCalledTimes(1);
|
|
322
322
|
});
|
|
@@ -345,7 +345,7 @@ describe('ObjectQL Engine', () => {
|
|
|
345
345
|
{ id: 'u1', name: 'Alice' },
|
|
346
346
|
]);
|
|
347
347
|
|
|
348
|
-
const result = await engine.find('task', {
|
|
348
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
349
349
|
expect(result[0].assignee).toBeNull();
|
|
350
350
|
expect(result[1].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
351
351
|
});
|
|
@@ -376,7 +376,7 @@ describe('ObjectQL Engine', () => {
|
|
|
376
376
|
{ id: 'u2', name: 'Bob' },
|
|
377
377
|
]);
|
|
378
378
|
|
|
379
|
-
const result = await engine.find('task', {
|
|
379
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
380
380
|
|
|
381
381
|
// Verify only 2 unique IDs queried
|
|
382
382
|
expect(mockDriver.find).toHaveBeenLastCalledWith(
|
|
@@ -410,7 +410,7 @@ describe('ObjectQL Engine', () => {
|
|
|
410
410
|
])
|
|
411
411
|
.mockResolvedValueOnce([]); // No records found
|
|
412
412
|
|
|
413
|
-
const result = await engine.find('task', {
|
|
413
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
414
414
|
expect(result[0].assignee).toBe('u_deleted'); // Fallback to raw ID
|
|
415
415
|
});
|
|
416
416
|
|
|
@@ -441,7 +441,7 @@ describe('ObjectQL Engine', () => {
|
|
|
441
441
|
.mockResolvedValueOnce([{ id: 'u1', name: 'Alice' }])
|
|
442
442
|
.mockResolvedValueOnce([{ id: 'p1', name: 'Project X' }]);
|
|
443
443
|
|
|
444
|
-
const result = await engine.find('task', {
|
|
444
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' }, project: { object: 'project' } } });
|
|
445
445
|
|
|
446
446
|
expect(result[0].assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
447
447
|
expect(result[0].project).toEqual({ id: 'p1', name: 'Project X' });
|
|
@@ -470,7 +470,7 @@ describe('ObjectQL Engine', () => {
|
|
|
470
470
|
{ id: 'u1', name: 'Alice' },
|
|
471
471
|
]);
|
|
472
472
|
|
|
473
|
-
const result = await engine.findOne('task', {
|
|
473
|
+
const result = await engine.findOne('task', { expand: { assignee: { object: 'assignee' } } });
|
|
474
474
|
|
|
475
475
|
expect(result.assignee).toEqual({ id: 'u1', name: 'Alice' });
|
|
476
476
|
});
|
|
@@ -495,7 +495,7 @@ describe('ObjectQL Engine', () => {
|
|
|
495
495
|
{ id: 't1', assignee: { id: 'u1', name: 'Alice' } },
|
|
496
496
|
]);
|
|
497
497
|
|
|
498
|
-
const result = await engine.find('task', {
|
|
498
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
499
499
|
|
|
500
500
|
// No expand query should have been made — the value was already an object
|
|
501
501
|
expect(mockDriver.find).toHaveBeenCalledTimes(1);
|
|
@@ -523,7 +523,7 @@ describe('ObjectQL Engine', () => {
|
|
|
523
523
|
])
|
|
524
524
|
.mockRejectedValueOnce(new Error('Driver connection failed'));
|
|
525
525
|
|
|
526
|
-
const result = await engine.find('task', {
|
|
526
|
+
const result = await engine.find('task', { expand: { assignee: { object: 'assignee' } } });
|
|
527
527
|
expect(result[0].assignee).toBe('u1'); // Kept raw ID
|
|
528
528
|
});
|
|
529
529
|
|
|
@@ -551,7 +551,7 @@ describe('ObjectQL Engine', () => {
|
|
|
551
551
|
{ id: 'u2', name: 'Bob' },
|
|
552
552
|
]);
|
|
553
553
|
|
|
554
|
-
const result = await engine.find('task', {
|
|
554
|
+
const result = await engine.find('task', { expand: { watchers: { object: 'watchers' } } });
|
|
555
555
|
expect(result[0].watchers).toEqual([
|
|
556
556
|
{ id: 'u1', name: 'Alice' },
|
|
557
557
|
{ id: 'u2', name: 'Bob' },
|
|
@@ -574,7 +574,7 @@ describe('ObjectQL Engine', () => {
|
|
|
574
574
|
.mockResolvedValueOnce([{ id: 'p1', org: 'o1' }]); // expand project (depth 0)
|
|
575
575
|
// org should NOT be expanded further — flat populate doesn't create nested expand
|
|
576
576
|
|
|
577
|
-
const result = await engine.find('task', {
|
|
577
|
+
const result = await engine.find('task', { expand: { project: { object: 'project' } } });
|
|
578
578
|
|
|
579
579
|
// Project expanded, but org inside project remains as raw ID
|
|
580
580
|
expect(result[0].project).toEqual({ id: 'p1', org: 'o1' });
|
package/src/engine.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { QueryAST, HookContext, ServiceObject } from '@objectstack/spec/data';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
EngineQueryOptions,
|
|
6
6
|
DataEngineInsertOptions,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
EngineUpdateOptions,
|
|
8
|
+
EngineDeleteOptions,
|
|
9
|
+
EngineAggregateOptions,
|
|
10
|
+
EngineCountOptions
|
|
11
11
|
} from '@objectstack/spec/data';
|
|
12
12
|
import { ExecutionContext, ExecutionContextSchema } from '@objectstack/spec/kernel';
|
|
13
13
|
import { DriverInterface, IDataEngine, Logger, createLogger } from '@objectstack/core';
|
|
@@ -741,59 +741,22 @@ export class ObjectQL implements IDataEngine {
|
|
|
741
741
|
return records;
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
-
// ============================================
|
|
745
|
-
// Helper: Query Conversion
|
|
746
|
-
// ============================================
|
|
747
|
-
|
|
748
|
-
private toQueryAST(object: string, options?: DataEngineQueryOptions): QueryAST {
|
|
749
|
-
const ast: QueryAST = { object };
|
|
750
|
-
if (!options) return ast;
|
|
751
|
-
|
|
752
|
-
if (options.filter) {
|
|
753
|
-
ast.where = options.filter;
|
|
754
|
-
}
|
|
755
|
-
if (options.select) {
|
|
756
|
-
ast.fields = options.select;
|
|
757
|
-
}
|
|
758
|
-
if (options.sort) {
|
|
759
|
-
// Support DataEngineSortSchema variant
|
|
760
|
-
if (Array.isArray(options.sort)) {
|
|
761
|
-
// [{ field: 'a', order: 'asc' }]
|
|
762
|
-
ast.orderBy = options.sort;
|
|
763
|
-
} else {
|
|
764
|
-
// Record<string, 'asc' | 'desc' | 1 | -1>
|
|
765
|
-
ast.orderBy = Object.entries(options.sort).map(([field, order]) => ({
|
|
766
|
-
field,
|
|
767
|
-
order: (order === -1 || order === 'desc') ? 'desc' : 'asc'
|
|
768
|
-
}));
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if (options.top !== undefined) ast.limit = options.top;
|
|
773
|
-
else if (options.limit !== undefined) ast.limit = options.limit;
|
|
774
|
-
|
|
775
|
-
if (options.skip !== undefined) ast.offset = options.skip;
|
|
776
|
-
|
|
777
|
-
// Map populate (relationship field names) to QueryAST expand entries
|
|
778
|
-
if (options.populate && options.populate.length > 0) {
|
|
779
|
-
ast.expand = {};
|
|
780
|
-
for (const rel of options.populate) {
|
|
781
|
-
ast.expand[rel] = { object: rel };
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return ast;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
744
|
// ============================================
|
|
789
745
|
// Data Access Methods (IDataEngine Interface)
|
|
790
746
|
// ============================================
|
|
791
747
|
|
|
792
|
-
async find(object: string, query?:
|
|
748
|
+
async find(object: string, query?: EngineQueryOptions): Promise<any[]> {
|
|
793
749
|
object = this.resolveObjectName(object);
|
|
794
750
|
this.logger.debug('Find operation starting', { object, query });
|
|
795
751
|
const driver = this.getDriver(object);
|
|
796
|
-
const ast =
|
|
752
|
+
const ast: QueryAST = { object, ...query };
|
|
753
|
+
// Remove context from the AST — it's not a driver concern
|
|
754
|
+
delete (ast as any).context;
|
|
755
|
+
// Normalize OData `top` alias → standard `limit`
|
|
756
|
+
if ((ast as any).top != null && ast.limit == null) {
|
|
757
|
+
ast.limit = (ast as any).top;
|
|
758
|
+
}
|
|
759
|
+
delete (ast as any).top;
|
|
797
760
|
|
|
798
761
|
const opCtx: OperationContext = {
|
|
799
762
|
object,
|
|
@@ -836,12 +799,14 @@ export class ObjectQL implements IDataEngine {
|
|
|
836
799
|
return opCtx.result as any[];
|
|
837
800
|
}
|
|
838
801
|
|
|
839
|
-
async findOne(objectName: string, query?:
|
|
802
|
+
async findOne(objectName: string, query?: EngineQueryOptions): Promise<any> {
|
|
840
803
|
objectName = this.resolveObjectName(objectName);
|
|
841
804
|
this.logger.debug('FindOne operation', { objectName });
|
|
842
805
|
const driver = this.getDriver(objectName);
|
|
843
|
-
const ast =
|
|
844
|
-
|
|
806
|
+
const ast: QueryAST = { object: objectName, ...query, limit: 1 };
|
|
807
|
+
// Remove context and top alias from the AST
|
|
808
|
+
delete (ast as any).context;
|
|
809
|
+
delete (ast as any).top;
|
|
845
810
|
|
|
846
811
|
const opCtx: OperationContext = {
|
|
847
812
|
object: objectName,
|
|
@@ -918,16 +883,15 @@ export class ObjectQL implements IDataEngine {
|
|
|
918
883
|
return opCtx.result;
|
|
919
884
|
}
|
|
920
885
|
|
|
921
|
-
async update(object: string, data: any, options?:
|
|
886
|
+
async update(object: string, data: any, options?: EngineUpdateOptions): Promise<any> {
|
|
922
887
|
object = this.resolveObjectName(object);
|
|
923
888
|
this.logger.debug('Update operation starting', { object });
|
|
924
889
|
const driver = this.getDriver(object);
|
|
925
890
|
|
|
926
|
-
// 1. Extract ID from data or
|
|
891
|
+
// 1. Extract ID from data or where if it's a single update by ID
|
|
927
892
|
let id = data.id;
|
|
928
|
-
if (!id && options?.
|
|
929
|
-
|
|
930
|
-
else if (options.filter.id) id = options.filter.id;
|
|
893
|
+
if (!id && options?.where && typeof options.where === 'object' && 'id' in options.where) {
|
|
894
|
+
id = (options.where as Record<string, unknown>).id;
|
|
931
895
|
}
|
|
932
896
|
|
|
933
897
|
const opCtx: OperationContext = {
|
|
@@ -954,7 +918,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
954
918
|
if (hookContext.input.id) {
|
|
955
919
|
result = await driver.update(object, hookContext.input.id as string, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
|
|
956
920
|
} else if (options?.multi && driver.updateMany) {
|
|
957
|
-
const ast =
|
|
921
|
+
const ast: QueryAST = { object, where: options.where };
|
|
958
922
|
result = await driver.updateMany(object, ast, hookContext.input.data as Record<string, unknown>, hookContext.input.options as any);
|
|
959
923
|
} else {
|
|
960
924
|
throw new Error('Update requires an ID or options.multi=true');
|
|
@@ -973,16 +937,15 @@ export class ObjectQL implements IDataEngine {
|
|
|
973
937
|
return opCtx.result;
|
|
974
938
|
}
|
|
975
939
|
|
|
976
|
-
async delete(object: string, options?:
|
|
940
|
+
async delete(object: string, options?: EngineDeleteOptions): Promise<any> {
|
|
977
941
|
object = this.resolveObjectName(object);
|
|
978
942
|
this.logger.debug('Delete operation starting', { object });
|
|
979
943
|
const driver = this.getDriver(object);
|
|
980
944
|
|
|
981
945
|
// Extract ID logic similar to update
|
|
982
946
|
let id: any = undefined;
|
|
983
|
-
if (options?.
|
|
984
|
-
|
|
985
|
-
else if (options.filter.id) id = options.filter.id;
|
|
947
|
+
if (options?.where && typeof options.where === 'object' && 'id' in options.where) {
|
|
948
|
+
id = (options.where as Record<string, unknown>).id;
|
|
986
949
|
}
|
|
987
950
|
|
|
988
951
|
const opCtx: OperationContext = {
|
|
@@ -1008,7 +971,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
1008
971
|
if (hookContext.input.id) {
|
|
1009
972
|
result = await driver.delete(object, hookContext.input.id as string, hookContext.input.options as any);
|
|
1010
973
|
} else if (options?.multi && driver.deleteMany) {
|
|
1011
|
-
const ast =
|
|
974
|
+
const ast: QueryAST = { object, where: options.where };
|
|
1012
975
|
result = await driver.deleteMany(object, ast, hookContext.input.options as any);
|
|
1013
976
|
} else {
|
|
1014
977
|
throw new Error('Delete requires an ID or options.multi=true');
|
|
@@ -1027,7 +990,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
1027
990
|
return opCtx.result;
|
|
1028
991
|
}
|
|
1029
992
|
|
|
1030
|
-
async count(object: string, query?:
|
|
993
|
+
async count(object: string, query?: EngineCountOptions): Promise<number> {
|
|
1031
994
|
object = this.resolveObjectName(object);
|
|
1032
995
|
const driver = this.getDriver(object);
|
|
1033
996
|
|
|
@@ -1040,18 +1003,18 @@ export class ObjectQL implements IDataEngine {
|
|
|
1040
1003
|
|
|
1041
1004
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
1042
1005
|
if (driver.count) {
|
|
1043
|
-
const ast =
|
|
1006
|
+
const ast: QueryAST = { object, where: query?.where };
|
|
1044
1007
|
return driver.count(object, ast);
|
|
1045
1008
|
}
|
|
1046
1009
|
// Fallback to find().length
|
|
1047
|
-
const res = await this.find(object, {
|
|
1010
|
+
const res = await this.find(object, { where: query?.where, fields: ['id'] });
|
|
1048
1011
|
return res.length;
|
|
1049
1012
|
});
|
|
1050
1013
|
|
|
1051
1014
|
return opCtx.result as number;
|
|
1052
1015
|
}
|
|
1053
1016
|
|
|
1054
|
-
async aggregate(object: string, query:
|
|
1017
|
+
async aggregate(object: string, query: EngineAggregateOptions): Promise<any[]> {
|
|
1055
1018
|
object = this.resolveObjectName(object);
|
|
1056
1019
|
const driver = this.getDriver(object);
|
|
1057
1020
|
this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
|
|
@@ -1066,13 +1029,9 @@ export class ObjectQL implements IDataEngine {
|
|
|
1066
1029
|
await this.executeWithMiddleware(opCtx, async () => {
|
|
1067
1030
|
const ast: QueryAST = {
|
|
1068
1031
|
object,
|
|
1069
|
-
where: query.
|
|
1032
|
+
where: query.where,
|
|
1070
1033
|
groupBy: query.groupBy,
|
|
1071
|
-
aggregations: query.aggregations
|
|
1072
|
-
function: agg.method,
|
|
1073
|
-
field: agg.field,
|
|
1074
|
-
alias: agg.alias || `${agg.method}_${agg.field || 'all'}`,
|
|
1075
|
-
})),
|
|
1034
|
+
aggregations: query.aggregations,
|
|
1076
1035
|
};
|
|
1077
1036
|
|
|
1078
1037
|
return driver.find(object, ast);
|
|
@@ -1355,7 +1314,7 @@ export class ObjectRepository {
|
|
|
1355
1314
|
/** Update a single record by ID */
|
|
1356
1315
|
async updateById(id: string | number, data: any): Promise<any> {
|
|
1357
1316
|
return this.engine.update(this.objectName, { ...data, id: id }, {
|
|
1358
|
-
|
|
1317
|
+
where: { id: id },
|
|
1359
1318
|
context: this.context,
|
|
1360
1319
|
});
|
|
1361
1320
|
}
|
|
@@ -1370,7 +1329,7 @@ export class ObjectRepository {
|
|
|
1370
1329
|
/** Delete a single record by ID */
|
|
1371
1330
|
async deleteById(id: string | number): Promise<any> {
|
|
1372
1331
|
return this.engine.delete(this.objectName, {
|
|
1373
|
-
|
|
1332
|
+
where: { id: id },
|
|
1374
1333
|
context: this.context,
|
|
1375
1334
|
});
|
|
1376
1335
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -175,7 +175,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
175
175
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
176
176
|
try {
|
|
177
177
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
178
|
-
|
|
178
|
+
where: { id: hookCtx.input.id }
|
|
179
179
|
});
|
|
180
180
|
if (existing) {
|
|
181
181
|
hookCtx.previous = existing;
|
|
@@ -191,7 +191,7 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
191
191
|
if (hookCtx.input?.id && !hookCtx.previous) {
|
|
192
192
|
try {
|
|
193
193
|
const existing = await this.ql!.findOne(hookCtx.object, {
|
|
194
|
-
|
|
194
|
+
where: { id: hookCtx.input.id }
|
|
195
195
|
});
|
|
196
196
|
if (existing) {
|
|
197
197
|
hookCtx.previous = existing;
|
|
@@ -25,87 +25,88 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
25
25
|
// ═══════════════════════════════════════════════════════════════
|
|
26
26
|
|
|
27
27
|
describe('findData', () => {
|
|
28
|
-
it('should normalize expand string to
|
|
29
|
-
await protocol.findData({ object: 'order_item', query: { expand: 'order,product' } });
|
|
28
|
+
it('should normalize $expand (OData) string to expand Record', async () => {
|
|
29
|
+
await protocol.findData({ object: 'order_item', query: { $expand: 'order,product' } });
|
|
30
30
|
|
|
31
31
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
32
32
|
'order_item',
|
|
33
33
|
expect.objectContaining({
|
|
34
|
-
|
|
34
|
+
expand: { order: { object: 'order' }, product: { object: 'product' } },
|
|
35
35
|
}),
|
|
36
36
|
);
|
|
37
|
-
// expand should be deleted from options
|
|
37
|
+
// $expand should be deleted from options
|
|
38
38
|
const callArgs = mockEngine.find.mock.calls[0][1];
|
|
39
|
-
expect(callArgs.expand).toBeUndefined();
|
|
40
39
|
expect(callArgs.$expand).toBeUndefined();
|
|
41
40
|
});
|
|
42
41
|
|
|
43
|
-
it('should normalize $expand (OData) to
|
|
42
|
+
it('should normalize $expand (OData) with different fields to expand Record', async () => {
|
|
44
43
|
await protocol.findData({ object: 'task', query: { $expand: 'assignee,project' } });
|
|
45
44
|
|
|
46
45
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
47
46
|
'task',
|
|
48
47
|
expect.objectContaining({
|
|
49
|
-
|
|
48
|
+
expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
|
|
50
49
|
}),
|
|
51
50
|
);
|
|
52
51
|
});
|
|
53
52
|
|
|
54
|
-
it('should
|
|
53
|
+
it('should normalize populate array to expand Record', async () => {
|
|
55
54
|
await protocol.findData({ object: 'task', query: { populate: ['assignee'] } });
|
|
56
55
|
|
|
57
56
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
58
57
|
'task',
|
|
59
58
|
expect.objectContaining({
|
|
60
|
-
|
|
59
|
+
expand: { assignee: { object: 'assignee' } },
|
|
61
60
|
}),
|
|
62
61
|
);
|
|
63
62
|
});
|
|
64
63
|
|
|
65
|
-
it('should normalize populate string to
|
|
64
|
+
it('should normalize populate string to expand Record', async () => {
|
|
66
65
|
await protocol.findData({ object: 'task', query: { populate: 'assignee,project' } });
|
|
67
66
|
|
|
68
67
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
69
68
|
'task',
|
|
70
69
|
expect.objectContaining({
|
|
71
|
-
|
|
70
|
+
expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
|
|
72
71
|
}),
|
|
73
72
|
);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
|
-
it('should prefer
|
|
75
|
+
it('should prefer populate names over expand string when both provided', async () => {
|
|
77
76
|
await protocol.findData({
|
|
78
77
|
object: 'task',
|
|
79
78
|
query: { populate: ['assignee'], expand: 'project' },
|
|
80
79
|
});
|
|
81
80
|
|
|
82
|
-
// populate
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
81
|
+
// populate names take precedence; the non-object expand string is
|
|
82
|
+
// cleaned up first, then populate-derived names create the Record.
|
|
83
|
+
const callArgs = mockEngine.find.mock.calls[0][1];
|
|
84
|
+
expect(callArgs.populate).toBeUndefined();
|
|
85
|
+
expect(callArgs.$expand).toBeUndefined();
|
|
86
|
+
expect(callArgs.expand).toEqual({ assignee: { object: 'assignee' } });
|
|
89
87
|
});
|
|
90
88
|
|
|
91
|
-
it('should
|
|
92
|
-
await protocol.findData({
|
|
89
|
+
it('should pass expand Record object through as-is', async () => {
|
|
90
|
+
await protocol.findData({
|
|
91
|
+
object: 'task',
|
|
92
|
+
query: { expand: { owner: { object: 'owner' }, team: { object: 'team' } } },
|
|
93
|
+
});
|
|
93
94
|
|
|
94
95
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
95
96
|
'task',
|
|
96
97
|
expect.objectContaining({
|
|
97
|
-
|
|
98
|
+
expand: { owner: { object: 'owner' }, team: { object: 'team' } },
|
|
98
99
|
}),
|
|
99
100
|
);
|
|
100
101
|
});
|
|
101
102
|
|
|
102
|
-
it('should normalize select string to array', async () => {
|
|
103
|
+
it('should normalize select string to fields array', async () => {
|
|
103
104
|
await protocol.findData({ object: 'task', query: { select: 'name,status,assignee' } });
|
|
104
105
|
|
|
105
106
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
106
107
|
'task',
|
|
107
108
|
expect.objectContaining({
|
|
108
|
-
|
|
109
|
+
fields: ['name', 'status', 'assignee'],
|
|
109
110
|
}),
|
|
110
111
|
);
|
|
111
112
|
});
|
|
@@ -116,8 +117,8 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
116
117
|
expect(mockEngine.find).toHaveBeenCalledWith(
|
|
117
118
|
'task',
|
|
118
119
|
expect.objectContaining({
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
limit: 10,
|
|
121
|
+
offset: 20,
|
|
121
122
|
}),
|
|
122
123
|
);
|
|
123
124
|
});
|
|
@@ -148,7 +149,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
148
149
|
// ═══════════════════════════════════════════════════════════════
|
|
149
150
|
|
|
150
151
|
describe('getData', () => {
|
|
151
|
-
it('should convert expand string to
|
|
152
|
+
it('should convert expand string to expand Record', async () => {
|
|
152
153
|
mockEngine.findOne.mockResolvedValue({ id: 'oi_1', name: 'Item 1' });
|
|
153
154
|
|
|
154
155
|
await protocol.getData({ object: 'order_item', id: 'oi_1', expand: 'order,product' });
|
|
@@ -156,13 +157,13 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
156
157
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
157
158
|
'order_item',
|
|
158
159
|
expect.objectContaining({
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
where: { id: 'oi_1' },
|
|
161
|
+
expand: { order: { object: 'order' }, product: { object: 'product' } },
|
|
161
162
|
}),
|
|
162
163
|
);
|
|
163
164
|
});
|
|
164
165
|
|
|
165
|
-
it('should convert expand array to
|
|
166
|
+
it('should convert expand array to expand Record', async () => {
|
|
166
167
|
mockEngine.findOne.mockResolvedValue({ id: 't1' });
|
|
167
168
|
|
|
168
169
|
await protocol.getData({ object: 'task', id: 't1', expand: ['assignee', 'project'] });
|
|
@@ -170,12 +171,13 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
170
171
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
171
172
|
'task',
|
|
172
173
|
expect.objectContaining({
|
|
173
|
-
|
|
174
|
+
where: { id: 't1' },
|
|
175
|
+
expand: { assignee: { object: 'assignee' }, project: { object: 'project' } },
|
|
174
176
|
}),
|
|
175
177
|
);
|
|
176
178
|
});
|
|
177
179
|
|
|
178
|
-
it('should convert select string to array', async () => {
|
|
180
|
+
it('should convert select string to fields array', async () => {
|
|
179
181
|
mockEngine.findOne.mockResolvedValue({ id: 't1', name: 'Test' });
|
|
180
182
|
|
|
181
183
|
await protocol.getData({ object: 'task', id: 't1', select: 'name,status' });
|
|
@@ -183,12 +185,13 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
183
185
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
184
186
|
'task',
|
|
185
187
|
expect.objectContaining({
|
|
186
|
-
|
|
188
|
+
where: { id: 't1' },
|
|
189
|
+
fields: ['name', 'status'],
|
|
187
190
|
}),
|
|
188
191
|
);
|
|
189
192
|
});
|
|
190
193
|
|
|
191
|
-
it('should pass both expand and
|
|
194
|
+
it('should pass both expand and fields together', async () => {
|
|
192
195
|
mockEngine.findOne.mockResolvedValue({ id: 'oi_1' });
|
|
193
196
|
|
|
194
197
|
await protocol.getData({
|
|
@@ -201,9 +204,9 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
201
204
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
202
205
|
'order_item',
|
|
203
206
|
expect.objectContaining({
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
where: { id: 'oi_1' },
|
|
208
|
+
expand: { order: { object: 'order' } },
|
|
209
|
+
fields: ['name', 'total'],
|
|
207
210
|
}),
|
|
208
211
|
);
|
|
209
212
|
});
|
|
@@ -215,7 +218,7 @@ describe('ObjectStackProtocolImplementation - Data Operations', () => {
|
|
|
215
218
|
|
|
216
219
|
expect(mockEngine.findOne).toHaveBeenCalledWith(
|
|
217
220
|
'task',
|
|
218
|
-
{
|
|
221
|
+
{ where: { id: 't1' } },
|
|
219
222
|
);
|
|
220
223
|
});
|
|
221
224
|
|