@objectql/core 4.0.6 → 4.2.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 +19 -0
- package/dist/ai/index.d.ts +8 -0
- package/dist/ai/index.js +25 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/registry.d.ts +23 -0
- package/dist/ai/registry.js +78 -0
- package/dist/ai/registry.js.map +1 -0
- package/dist/app.d.ts +2 -1
- package/dist/app.js +41 -20
- package/dist/app.js.map +1 -1
- package/dist/gateway.d.ts +3 -2
- package/dist/gateway.js +6 -2
- package/dist/gateway.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/optimizations/CompiledHookManager.d.ts +1 -0
- package/dist/optimizations/CompiledHookManager.js +7 -1
- package/dist/optimizations/CompiledHookManager.js.map +1 -1
- package/dist/plugin.d.ts +3 -2
- package/dist/plugin.js +77 -33
- package/dist/plugin.js.map +1 -1
- package/dist/protocol.d.ts +11 -0
- package/dist/protocol.js +12 -0
- package/dist/protocol.js.map +1 -1
- package/dist/query/filter-translator.d.ts +1 -4
- package/dist/query/filter-translator.js.map +1 -1
- package/dist/query/query-analyzer.d.ts +1 -3
- package/dist/query/query-analyzer.js.map +1 -1
- package/dist/query/query-builder.d.ts +1 -9
- package/dist/query/query-builder.js +8 -0
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-service.d.ts +2 -3
- 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 +18 -173
- package/dist/repository.js.map +1 -1
- package/package.json +16 -9
- package/src/ai/index.ts +9 -0
- package/src/ai/registry.ts +81 -0
- package/src/app.ts +47 -20
- package/src/gateway.ts +7 -3
- package/src/index.ts +24 -7
- package/src/optimizations/CompiledHookManager.ts +9 -1
- package/src/plugin.ts +90 -36
- package/src/protocol.ts +13 -0
- package/src/query/filter-translator.ts +3 -3
- package/src/query/query-analyzer.ts +1 -2
- package/src/query/query-builder.ts +8 -8
- package/src/query/query-service.ts +7 -3
- package/src/repository.ts +20 -223
- package/test/ai-registry.test.ts +42 -0
- package/test/app.test.ts +6 -6
- package/test/optimizations.test.ts +1 -1
- package/test/plugin-integration.test.ts +3 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/jest.config.js +0 -29
package/src/repository.ts
CHANGED
|
@@ -6,69 +6,21 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, ActionContext, HookAPI, RetrievalHookContext, MutationHookContext, UpdateHookContext, ValidationContext, ValidationError, ValidationRuleResult, FormulaContext, Filter } from '@objectql/types';
|
|
9
|
+
import { ObjectQLContext, IObjectQL, ObjectConfig, Driver, UnifiedQuery, ActionContext, HookAPI, RetrievalHookContext, MutationHookContext, UpdateHookContext, ValidationContext, ValidationError, ValidationRuleResult, FormulaContext, Filter, QueryAST } from '@objectql/types';
|
|
10
10
|
import type { ObjectKernel } from '@objectstack/runtime';
|
|
11
11
|
import { Data } from '@objectstack/spec';
|
|
12
|
-
|
|
13
|
-
type SortNode = Data.
|
|
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
|
-
}
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
type SortNode = z.infer<typeof Data.SortNodeSchema>;
|
|
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
|
-
}
|
|
45
|
-
|
|
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
22
|
}
|
|
58
23
|
|
|
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,15 @@ export class ObjectRepository {
|
|
|
354
148
|
};
|
|
355
149
|
await this.app.triggerHook('beforeCount', this.objectName, hookCtx);
|
|
356
150
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
151
|
+
// Execute via kernel (delegates to QueryService)
|
|
152
|
+
let result: number;
|
|
153
|
+
const kernel = this.getKernel() as any;
|
|
154
|
+
if (typeof kernel.count === 'function') {
|
|
155
|
+
result = await kernel.count(this.objectName, hookCtx.query || {});
|
|
156
|
+
} else {
|
|
157
|
+
// Fallback to driver if kernel doesn't support count
|
|
158
|
+
result = await this.getDriver().count(this.objectName, hookCtx.query || {});
|
|
159
|
+
}
|
|
361
160
|
|
|
362
161
|
hookCtx.result = result;
|
|
363
162
|
await this.app.triggerHook('afterCount', this.objectName, hookCtx);
|
|
@@ -380,8 +179,7 @@ export class ObjectRepository {
|
|
|
380
179
|
if (this.context.userId) finalDoc.created_by = this.context.userId;
|
|
381
180
|
if (this.context.spaceId) finalDoc.space_id = this.context.spaceId;
|
|
382
181
|
|
|
383
|
-
//
|
|
384
|
-
await this.validateRecord('create', finalDoc);
|
|
182
|
+
// Validation moved to ValidatorPlugin hook
|
|
385
183
|
|
|
386
184
|
// Execute via kernel
|
|
387
185
|
const result = await (this.getKernel() as any).create(this.objectName, finalDoc, this.getOptions());
|
|
@@ -407,8 +205,7 @@ export class ObjectRepository {
|
|
|
407
205
|
};
|
|
408
206
|
await this.app.triggerHook('beforeUpdate', this.objectName, hookCtx);
|
|
409
207
|
|
|
410
|
-
//
|
|
411
|
-
await this.validateRecord('update', hookCtx.data, previousData);
|
|
208
|
+
// Validation moved to ValidatorPlugin hook
|
|
412
209
|
|
|
413
210
|
// Execute via kernel
|
|
414
211
|
const result = await (this.getKernel() as any).update(this.objectName, String(id), hookCtx.data, this.getOptions());
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL - AI Registry Tests
|
|
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 { createDefaultAiRegistry } from '../src/ai';
|
|
10
|
+
|
|
11
|
+
describe('AI Registry', () => {
|
|
12
|
+
it('registers and retrieves models', () => {
|
|
13
|
+
const registry = createDefaultAiRegistry();
|
|
14
|
+
|
|
15
|
+
registry.models.register({
|
|
16
|
+
id: 'embed-small',
|
|
17
|
+
provider: 'openai',
|
|
18
|
+
model: 'text-embedding-3-small',
|
|
19
|
+
type: 'embedding',
|
|
20
|
+
embeddingDimensions: 1536,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const model = registry.models.get('embed-small');
|
|
24
|
+
expect(model?.model).toBe('text-embedding-3-small');
|
|
25
|
+
expect(registry.models.list().length).toBe(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('registers and retrieves prompt templates', () => {
|
|
29
|
+
const registry = createDefaultAiRegistry();
|
|
30
|
+
|
|
31
|
+
registry.prompts.register({
|
|
32
|
+
id: 'greeting',
|
|
33
|
+
template: 'Hello, {{name}}!',
|
|
34
|
+
variables: ['name'],
|
|
35
|
+
version: 'v1',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const prompt = registry.prompts.get('greeting', 'v1');
|
|
39
|
+
expect(prompt?.template).toBe('Hello, {{name}}!');
|
|
40
|
+
expect(registry.prompts.list('greeting').length).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
});
|
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', () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
9
|
+
// import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
10
10
|
import {
|
|
11
11
|
OptimizedMetadataRegistry,
|
|
12
12
|
QueryCompiler,
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
import { ObjectQLPlugin } from '../src/plugin';
|
|
10
10
|
import { ValidatorPlugin } from '@objectql/plugin-validator';
|
|
11
11
|
import { FormulaPlugin } from '@objectql/plugin-formula';
|
|
12
|
+
import { vi } from 'vitest';
|
|
12
13
|
|
|
13
14
|
// Mock the sub-plugins
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
vi.mock('@objectql/plugin-validator');
|
|
16
|
+
vi.mock('@objectql/plugin-formula');
|
|
16
17
|
|
|
17
18
|
describe('ObjectQLPlugin Integration', () => {
|
|
18
19
|
let plugin: ObjectQLPlugin;
|