@objectql/core 4.0.5 → 4.1.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 +1 -1
- package/CHANGELOG.md +9 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +48 -9
- package/dist/app.js.map +1 -1
- package/dist/gateway.d.ts +36 -0
- package/dist/gateway.js +89 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +21 -0
- package/dist/plugin.js.map +1 -1
- package/dist/protocol.d.ts +31 -0
- package/dist/protocol.js +36 -0
- package/dist/protocol.js.map +1 -1
- package/dist/query/query-service.d.ts +1 -0
- package/dist/query/query-service.js +5 -1
- package/dist/query/query-service.js.map +1 -1
- package/dist/repository.d.ts +0 -28
- package/dist/repository.js +10 -173
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -3
- package/package.json +8 -8
- package/src/app.ts +49 -9
- package/src/gateway.ts +101 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +24 -0
- package/src/protocol.ts +42 -0
- package/src/query/query-service.ts +6 -1
- package/src/repository.ts +10 -220
- package/test/__mocks__/@objectstack/core.ts +27 -0
- package/test/__mocks__/@objectstack/objectql.ts +45 -0
- package/test/app.test.ts +6 -6
- package/test/gateway.test.ts +88 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/protocol.ts
CHANGED
|
@@ -246,4 +246,46 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
246
246
|
// Not implemented in this shim yet
|
|
247
247
|
throw new Error('Action execution not implemented in protocol shim');
|
|
248
248
|
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Analytics Query - Execute analytics query
|
|
252
|
+
*/
|
|
253
|
+
async analyticsQuery(args: any): Promise<any> {
|
|
254
|
+
throw new Error('analyticsQuery not implemented');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get Analytics Metadata
|
|
259
|
+
*/
|
|
260
|
+
async getAnalyticsMeta(args: any): Promise<any> {
|
|
261
|
+
throw new Error('getAnalyticsMeta not implemented');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Trigger Automation
|
|
266
|
+
*/
|
|
267
|
+
async triggerAutomation(args: { trigger: string; payload: Record<string, any> }): Promise<{ success: boolean; jobId?: string; result?: any }> {
|
|
268
|
+
throw new Error('triggerAutomation not implemented');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* List Spaces (Hub/Workspace Management)
|
|
273
|
+
*/
|
|
274
|
+
async listSpaces(args: any): Promise<any> {
|
|
275
|
+
throw new Error('listSpaces not implemented');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Create Space (Hub/Workspace Management)
|
|
280
|
+
*/
|
|
281
|
+
async createSpace(args: any): Promise<any> {
|
|
282
|
+
throw new Error('createSpace not implemented');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Install Plugin (Hub/Extension Management)
|
|
287
|
+
*/
|
|
288
|
+
async installPlugin(args: any): Promise<any> {
|
|
289
|
+
throw new Error('installPlugin not implemented');
|
|
290
|
+
}
|
|
249
291
|
}
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
import { Data } from '@objectstack/spec';
|
|
17
17
|
type QueryAST = Data.QueryAST;
|
|
18
18
|
import { QueryBuilder } from './query-builder';
|
|
19
|
+
import { QueryCompiler } from '../optimizations/QueryCompiler';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Options for query execution
|
|
@@ -101,12 +102,14 @@ export interface QueryProfile {
|
|
|
101
102
|
*/
|
|
102
103
|
export class QueryService {
|
|
103
104
|
private queryBuilder: QueryBuilder;
|
|
105
|
+
private queryCompiler: QueryCompiler;
|
|
104
106
|
|
|
105
107
|
constructor(
|
|
106
108
|
private datasources: Record<string, Driver>,
|
|
107
109
|
private metadata: MetadataRegistry
|
|
108
110
|
) {
|
|
109
111
|
this.queryBuilder = new QueryBuilder();
|
|
112
|
+
this.queryCompiler = new QueryCompiler(1000);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
/**
|
|
@@ -142,7 +145,9 @@ export class QueryService {
|
|
|
142
145
|
* @private
|
|
143
146
|
*/
|
|
144
147
|
private buildQueryAST(objectName: string, query: UnifiedQuery): QueryAST {
|
|
145
|
-
|
|
148
|
+
const ast = this.queryBuilder.build(objectName, query);
|
|
149
|
+
const compiled = this.queryCompiler.compile(objectName, ast);
|
|
150
|
+
return compiled.ast;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
/**
|
package/src/repository.ts
CHANGED
|
@@ -11,64 +11,16 @@ import type { ObjectKernel } from '@objectstack/runtime';
|
|
|
11
11
|
import { Data } from '@objectstack/spec';
|
|
12
12
|
type QueryAST = Data.QueryAST;
|
|
13
13
|
type SortNode = Data.SortNode;
|
|
14
|
-
import { Validator } from '@objectql/plugin-validator';
|
|
15
|
-
import { FormulaEngine } from '@objectql/plugin-formula';
|
|
16
|
-
import { QueryBuilder } from './query';
|
|
17
|
-
import { QueryCompiler } from './optimizations/QueryCompiler';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Extended ObjectStack Kernel with optional ObjectQL plugin capabilities.
|
|
21
|
-
* These properties are attached by ValidatorPlugin and FormulaPlugin during installation.
|
|
22
|
-
*/
|
|
23
|
-
interface ExtendedKernel extends ObjectKernel {
|
|
24
|
-
validator?: Validator;
|
|
25
|
-
formulaEngine?: FormulaEngine;
|
|
26
|
-
create?: (objectName: string, data: any) => Promise<any>;
|
|
27
|
-
update?: (objectName: string, id: string, data: any) => Promise<any>;
|
|
28
|
-
delete?: (objectName: string, id: string) => Promise<any>;
|
|
29
|
-
find?: (objectName: string, query: any) => Promise<any>;
|
|
30
|
-
get?: (objectName: string, id: string) => Promise<any>;
|
|
31
|
-
}
|
|
32
14
|
|
|
33
15
|
export class ObjectRepository {
|
|
34
|
-
private queryBuilder: QueryBuilder;
|
|
35
|
-
// Shared query compiler for caching compiled queries
|
|
36
|
-
private static queryCompiler = new QueryCompiler(1000);
|
|
37
16
|
|
|
38
17
|
constructor(
|
|
39
18
|
private objectName: string,
|
|
40
19
|
private context: ObjectQLContext,
|
|
41
20
|
private app: IObjectQL
|
|
42
21
|
) {
|
|
43
|
-
this.queryBuilder = new QueryBuilder();
|
|
44
22
|
}
|
|
45
23
|
|
|
46
|
-
/**
|
|
47
|
-
* Get validator instance from kernel (via plugin)
|
|
48
|
-
* Falls back to creating a new instance if not available
|
|
49
|
-
*/
|
|
50
|
-
private getValidator(): Validator {
|
|
51
|
-
const kernel = this.getKernel() as ExtendedKernel;
|
|
52
|
-
if (kernel.validator) {
|
|
53
|
-
return kernel.validator;
|
|
54
|
-
}
|
|
55
|
-
// Fallback for backward compatibility
|
|
56
|
-
return new Validator();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Get formula engine instance from kernel (via plugin)
|
|
61
|
-
* Falls back to creating a new instance if not available
|
|
62
|
-
*/
|
|
63
|
-
private getFormulaEngine(): FormulaEngine {
|
|
64
|
-
const kernel = this.getKernel() as ExtendedKernel;
|
|
65
|
-
if (kernel.formulaEngine) {
|
|
66
|
-
return kernel.formulaEngine;
|
|
67
|
-
}
|
|
68
|
-
// Fallback for backward compatibility
|
|
69
|
-
return new FormulaEngine();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
24
|
private getDriver(): Driver {
|
|
73
25
|
const obj = this.getSchema();
|
|
74
26
|
const datasourceName = obj.datasource || 'default';
|
|
@@ -86,17 +38,6 @@ export class ObjectRepository {
|
|
|
86
38
|
};
|
|
87
39
|
}
|
|
88
40
|
|
|
89
|
-
/**
|
|
90
|
-
* Translates ObjectQL UnifiedQuery to ObjectStack QueryAST format
|
|
91
|
-
* Uses query compiler for caching and optimization
|
|
92
|
-
*/
|
|
93
|
-
private buildQueryAST(query: UnifiedQuery): QueryAST {
|
|
94
|
-
const ast = this.queryBuilder.build(this.objectName, query);
|
|
95
|
-
// Use query compiler to cache and optimize the AST
|
|
96
|
-
const compiled = ObjectRepository.queryCompiler.compile(this.objectName, ast);
|
|
97
|
-
return compiled.ast;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
41
|
getSchema(): ObjectConfig {
|
|
101
42
|
const obj = this.app.getObject(this.objectName);
|
|
102
43
|
if (!obj) {
|
|
@@ -129,148 +70,6 @@ export class ObjectRepository {
|
|
|
129
70
|
};
|
|
130
71
|
}
|
|
131
72
|
|
|
132
|
-
/**
|
|
133
|
-
* Validates a record against field-level and object-level validation rules.
|
|
134
|
-
* For updates, only fields present in the update payload are validated at the field level,
|
|
135
|
-
* while object-level rules use the merged record (previousRecord + updates).
|
|
136
|
-
*/
|
|
137
|
-
private async validateRecord(
|
|
138
|
-
operation: 'create' | 'update',
|
|
139
|
-
record: any,
|
|
140
|
-
previousRecord?: any
|
|
141
|
-
): Promise<void> {
|
|
142
|
-
const schema = this.getSchema();
|
|
143
|
-
const allResults: ValidationRuleResult[] = [];
|
|
144
|
-
|
|
145
|
-
// 1. Validate field-level rules
|
|
146
|
-
// For updates, only validate fields that are present in the update payload
|
|
147
|
-
for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
|
|
148
|
-
// Skip field validation for updates if the field is not in the update payload
|
|
149
|
-
if (operation === 'update' && !(fieldName in record)) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const value = record[fieldName];
|
|
154
|
-
const fieldResults = await this.getValidator().validateField(
|
|
155
|
-
fieldName,
|
|
156
|
-
fieldConfig,
|
|
157
|
-
value,
|
|
158
|
-
{
|
|
159
|
-
record,
|
|
160
|
-
previousRecord,
|
|
161
|
-
operation,
|
|
162
|
-
user: this.getUserFromContext(),
|
|
163
|
-
api: this.getHookAPI(),
|
|
164
|
-
}
|
|
165
|
-
);
|
|
166
|
-
allResults.push(...fieldResults);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 2. Validate object-level validation rules
|
|
170
|
-
if (schema.validation?.rules && schema.validation.rules.length > 0) {
|
|
171
|
-
// For updates, merge the update data with previous record to get the complete final state
|
|
172
|
-
const mergedRecord = operation === 'update' && previousRecord
|
|
173
|
-
? { ...previousRecord, ...record }
|
|
174
|
-
: record;
|
|
175
|
-
|
|
176
|
-
// Track which fields changed (using shallow comparison for performance)
|
|
177
|
-
// IMPORTANT: Shallow comparison does not detect changes in nested objects/arrays.
|
|
178
|
-
// If your validation rules rely on detecting changes in complex nested structures,
|
|
179
|
-
// you may need to implement custom change tracking in hooks.
|
|
180
|
-
const changedFields = previousRecord
|
|
181
|
-
? Object.keys(record).filter(key => record[key] !== previousRecord[key])
|
|
182
|
-
: undefined;
|
|
183
|
-
|
|
184
|
-
const validationContext: ValidationContext = {
|
|
185
|
-
record: mergedRecord,
|
|
186
|
-
previousRecord,
|
|
187
|
-
operation,
|
|
188
|
-
user: this.getUserFromContext(),
|
|
189
|
-
api: this.getHookAPI(),
|
|
190
|
-
changedFields,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const result = await this.getValidator().validate(schema.validation.rules, validationContext);
|
|
194
|
-
allResults.push(...result.results);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 3. Collect errors and throw if any
|
|
198
|
-
const errors = allResults.filter(r => !r.valid && r.severity === 'error');
|
|
199
|
-
if (errors.length > 0) {
|
|
200
|
-
const errorMessage = errors.map(e => e.message).join('; ');
|
|
201
|
-
throw new ValidationError(errorMessage, errors);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Evaluate formula fields for a record
|
|
207
|
-
* Adds computed formula field values to the record
|
|
208
|
-
*/
|
|
209
|
-
private evaluateFormulas(record: any): any {
|
|
210
|
-
const schema = this.getSchema();
|
|
211
|
-
const now = new Date();
|
|
212
|
-
|
|
213
|
-
// Build formula context
|
|
214
|
-
const formulaContext: FormulaContext = {
|
|
215
|
-
record,
|
|
216
|
-
system: {
|
|
217
|
-
today: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
|
|
218
|
-
now: now,
|
|
219
|
-
year: now.getFullYear(),
|
|
220
|
-
month: now.getMonth() + 1,
|
|
221
|
-
day: now.getDate(),
|
|
222
|
-
hour: now.getHours(),
|
|
223
|
-
minute: now.getMinutes(),
|
|
224
|
-
second: now.getSeconds(),
|
|
225
|
-
},
|
|
226
|
-
current_user: {
|
|
227
|
-
id: this.context.userId || '',
|
|
228
|
-
// TODO: Retrieve actual user name from user object if available
|
|
229
|
-
name: undefined,
|
|
230
|
-
email: undefined,
|
|
231
|
-
role: this.context.roles?.[0],
|
|
232
|
-
},
|
|
233
|
-
is_new: false,
|
|
234
|
-
record_id: record._id || record.id,
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// Evaluate each formula field
|
|
238
|
-
for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
|
|
239
|
-
const formulaExpression = fieldConfig.expression;
|
|
240
|
-
|
|
241
|
-
if (fieldConfig.type === 'formula' && formulaExpression) {
|
|
242
|
-
const result = this.getFormulaEngine().evaluate(
|
|
243
|
-
formulaExpression,
|
|
244
|
-
formulaContext,
|
|
245
|
-
fieldConfig.data_type || 'text',
|
|
246
|
-
{ strict: true }
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
if (result.success) {
|
|
250
|
-
record[fieldName] = result.value;
|
|
251
|
-
} else {
|
|
252
|
-
// In case of error, set to null and log for diagnostics
|
|
253
|
-
record[fieldName] = null;
|
|
254
|
-
// Formula evaluation should not throw here, but we need observability
|
|
255
|
-
// This logging is intentionally minimal and side-effect free
|
|
256
|
-
|
|
257
|
-
console.error(
|
|
258
|
-
'[ObjectQL][FormulaEngine] Formula evaluation failed',
|
|
259
|
-
{
|
|
260
|
-
objectName: this.objectName,
|
|
261
|
-
fieldName,
|
|
262
|
-
recordId: formulaContext.record_id,
|
|
263
|
-
expression: formulaExpression,
|
|
264
|
-
error: result.error,
|
|
265
|
-
stack: result.stack,
|
|
266
|
-
}
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return record;
|
|
273
|
-
}
|
|
274
73
|
|
|
275
74
|
async find(query: UnifiedQuery = {}): Promise<any[]> {
|
|
276
75
|
const hookCtx: RetrievalHookContext = {
|
|
@@ -284,15 +83,12 @@ export class ObjectRepository {
|
|
|
284
83
|
};
|
|
285
84
|
await this.app.triggerHook('beforeFind', this.objectName, hookCtx);
|
|
286
85
|
|
|
287
|
-
//
|
|
288
|
-
const
|
|
289
|
-
const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
|
|
86
|
+
// Execute via kernel (delegates to QueryService)
|
|
87
|
+
const kernelResult = await (this.getKernel() as any).find(this.objectName, hookCtx.query || {});
|
|
290
88
|
const results = kernelResult.value;
|
|
291
89
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
hookCtx.result = resultsWithFormulas;
|
|
90
|
+
// Formula evaluation moved to FormulaPlugin hook
|
|
91
|
+
hookCtx.result = results;
|
|
296
92
|
await this.app.triggerHook('afterFind', this.objectName, hookCtx);
|
|
297
93
|
|
|
298
94
|
return hookCtx.result as any[];
|
|
@@ -314,10 +110,8 @@ export class ObjectRepository {
|
|
|
314
110
|
// Use kernel.get() for direct ID lookup
|
|
315
111
|
const result = await (this.getKernel() as any).get(this.objectName, String(idOrQuery));
|
|
316
112
|
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
hookCtx.result = resultWithFormulas;
|
|
113
|
+
// Formula evaluation moved to FormulaPlugin hook
|
|
114
|
+
hookCtx.result = result;
|
|
321
115
|
await this.app.triggerHook('afterFind', this.objectName, hookCtx);
|
|
322
116
|
return hookCtx.result;
|
|
323
117
|
} else {
|
|
@@ -354,10 +148,8 @@ export class ObjectRepository {
|
|
|
354
148
|
};
|
|
355
149
|
await this.app.triggerHook('beforeCount', this.objectName, hookCtx);
|
|
356
150
|
|
|
357
|
-
//
|
|
358
|
-
const
|
|
359
|
-
const kernelResult = await (this.getKernel() as any).find(this.objectName, ast);
|
|
360
|
-
const result = kernelResult.count;
|
|
151
|
+
// Execute via kernel (delegates to QueryService)
|
|
152
|
+
const result = await (this.getKernel() as any).count(this.objectName, hookCtx.query || {});
|
|
361
153
|
|
|
362
154
|
hookCtx.result = result;
|
|
363
155
|
await this.app.triggerHook('afterCount', this.objectName, hookCtx);
|
|
@@ -380,8 +172,7 @@ export class ObjectRepository {
|
|
|
380
172
|
if (this.context.userId) finalDoc.created_by = this.context.userId;
|
|
381
173
|
if (this.context.spaceId) finalDoc.space_id = this.context.spaceId;
|
|
382
174
|
|
|
383
|
-
//
|
|
384
|
-
await this.validateRecord('create', finalDoc);
|
|
175
|
+
// Validation moved to ValidatorPlugin hook
|
|
385
176
|
|
|
386
177
|
// Execute via kernel
|
|
387
178
|
const result = await (this.getKernel() as any).create(this.objectName, finalDoc, this.getOptions());
|
|
@@ -407,8 +198,7 @@ export class ObjectRepository {
|
|
|
407
198
|
};
|
|
408
199
|
await this.app.triggerHook('beforeUpdate', this.objectName, hookCtx);
|
|
409
200
|
|
|
410
|
-
//
|
|
411
|
-
await this.validateRecord('update', hookCtx.data, previousData);
|
|
201
|
+
// Validation moved to ValidatorPlugin hook
|
|
412
202
|
|
|
413
203
|
// Execute via kernel
|
|
414
204
|
const result = await (this.getKernel() as any).update(this.objectName, String(id), hookCtx.data, this.getOptions());
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock for @objectstack/core to enable Jest testing
|
|
11
|
+
*
|
|
12
|
+
* Since @objectstack/core@0.9.2 uses ES modules with import.meta,
|
|
13
|
+
* which Jest doesn't support well, we provide this mock for testing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const createLogger = jest.fn(() => ({
|
|
17
|
+
trace: jest.fn(),
|
|
18
|
+
debug: jest.fn(),
|
|
19
|
+
info: jest.fn(),
|
|
20
|
+
warn: jest.fn(),
|
|
21
|
+
error: jest.fn(),
|
|
22
|
+
fatal: jest.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
export const ObjectKernel = jest.fn();
|
|
26
|
+
export const LiteKernel = jest.fn();
|
|
27
|
+
export const createApiRegistryPlugin = jest.fn();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Mock for @objectstack/objectql to enable Jest testing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class ObjectQL {
|
|
14
|
+
constructor(public config: any) {}
|
|
15
|
+
async connect() {}
|
|
16
|
+
async disconnect() {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const mockStore = new Map<string, Map<string, any>>();
|
|
20
|
+
|
|
21
|
+
export const SchemaRegistry = {
|
|
22
|
+
register: jest.fn(),
|
|
23
|
+
get: jest.fn(),
|
|
24
|
+
registerItem: jest.fn((type: string, item: any, keyField: string = 'name') => {
|
|
25
|
+
if (!mockStore.has(type)) {
|
|
26
|
+
mockStore.set(type, new Map());
|
|
27
|
+
}
|
|
28
|
+
const key = item[keyField];
|
|
29
|
+
mockStore.get(type)!.set(key, item);
|
|
30
|
+
}),
|
|
31
|
+
unregisterItem: jest.fn((type: string, name: string) => {
|
|
32
|
+
const collection = mockStore.get(type);
|
|
33
|
+
if (collection) {
|
|
34
|
+
collection.delete(name);
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
getItem: jest.fn((type: string, name: string) => {
|
|
38
|
+
return mockStore.get(type)?.get(name);
|
|
39
|
+
}),
|
|
40
|
+
listItems: jest.fn((type: string) => {
|
|
41
|
+
const items = mockStore.get(type);
|
|
42
|
+
return items ? Array.from(items.values()) : [];
|
|
43
|
+
}),
|
|
44
|
+
metadata: mockStore,
|
|
45
|
+
};
|
package/test/app.test.ts
CHANGED
|
@@ -295,12 +295,12 @@ describe('ObjectQL App', () => {
|
|
|
295
295
|
|
|
296
296
|
describe('Plugin System', () => {
|
|
297
297
|
it('should initialize runtime plugins on init', async () => {
|
|
298
|
-
const
|
|
299
|
-
const
|
|
298
|
+
const installFn = jest.fn();
|
|
299
|
+
const onStartFn = jest.fn();
|
|
300
300
|
const mockPlugin = {
|
|
301
301
|
name: 'test-plugin',
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
install: installFn,
|
|
303
|
+
onStart: onStartFn
|
|
304
304
|
};
|
|
305
305
|
|
|
306
306
|
const app = new ObjectQL({
|
|
@@ -309,8 +309,8 @@ describe('ObjectQL App', () => {
|
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
await app.init();
|
|
312
|
-
expect(
|
|
313
|
-
expect(
|
|
312
|
+
expect(installFn).toHaveBeenCalled();
|
|
313
|
+
expect(onStartFn).toHaveBeenCalled();
|
|
314
314
|
});
|
|
315
315
|
|
|
316
316
|
it('should use plugin method', () => {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL
|
|
3
|
+
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ObjectGateway } from '../src/gateway';
|
|
10
|
+
import { ApiRequest, ApiResponse, GatewayProtocol } from '@objectql/types';
|
|
11
|
+
|
|
12
|
+
describe('ObjectGateway', () => {
|
|
13
|
+
let gateway: ObjectGateway;
|
|
14
|
+
let mockProtocol: GatewayProtocol;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
gateway = new ObjectGateway();
|
|
18
|
+
mockProtocol = {
|
|
19
|
+
name: 'mock',
|
|
20
|
+
route: jest.fn().mockReturnValue(true),
|
|
21
|
+
handle: jest.fn().mockResolvedValue({ status: 200, body: 'ok' })
|
|
22
|
+
};
|
|
23
|
+
gateway.registerProtocol(mockProtocol);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should route request to registered protocol', async () => {
|
|
27
|
+
const req: ApiRequest = {
|
|
28
|
+
path: '/test',
|
|
29
|
+
method: 'GET',
|
|
30
|
+
headers: {},
|
|
31
|
+
query: {}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const response = await gateway.handle(req);
|
|
35
|
+
|
|
36
|
+
expect(mockProtocol.route).toHaveBeenCalledWith(req);
|
|
37
|
+
expect(mockProtocol.handle).toHaveBeenCalledWith(req);
|
|
38
|
+
expect(response.status).toBe(200);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return 404 if no protocol matches', async () => {
|
|
42
|
+
const specializedGateway = new ObjectGateway();
|
|
43
|
+
const response = await specializedGateway.handle({
|
|
44
|
+
path: '/unknown',
|
|
45
|
+
method: 'GET',
|
|
46
|
+
headers: {},
|
|
47
|
+
query: {}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(response.status).toBe(404);
|
|
51
|
+
expect(response.body.error.code).toBe('PROTOCOL_NOT_FOUND');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should apply request transformers', async () => {
|
|
55
|
+
const req: ApiRequest = {
|
|
56
|
+
path: '/original',
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: {},
|
|
59
|
+
query: {}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
gateway.addRequestTransform(async (r) => {
|
|
63
|
+
return { ...r, path: '/transformed' };
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await gateway.handle(req);
|
|
67
|
+
|
|
68
|
+
// Protocol should see the transformed request
|
|
69
|
+
expect(mockProtocol.route).toHaveBeenCalledWith(expect.objectContaining({ path: '/transformed' }));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should apply response transformers', async () => {
|
|
73
|
+
const req: ApiRequest = {
|
|
74
|
+
path: '/test',
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: {},
|
|
77
|
+
query: {}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
gateway.addResponseTransform(async (res) => {
|
|
81
|
+
return { ...res, headers: { ...res.headers, 'X-Custom': 'Added' } };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const response = await gateway.handle(req);
|
|
85
|
+
|
|
86
|
+
expect(response.headers?.['X-Custom']).toBe('Added');
|
|
87
|
+
});
|
|
88
|
+
});
|