@objectql/core 3.0.0 → 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/CHANGELOG.md +32 -0
- package/IMPLEMENTATION_STATUS.md +364 -0
- package/README.md +31 -9
- package/RUNTIME_INTEGRATION.md +391 -0
- package/dist/ai-agent.d.ts +4 -3
- package/dist/ai-agent.js +10 -3
- package/dist/ai-agent.js.map +1 -1
- package/dist/app.d.ts +29 -6
- package/dist/app.js +117 -58
- package/dist/app.js.map +1 -1
- package/dist/formula-engine.d.ts +7 -0
- package/dist/formula-engine.js +9 -2
- package/dist/formula-engine.js.map +1 -1
- package/dist/formula-plugin.d.ts +52 -0
- package/dist/formula-plugin.js +107 -0
- package/dist/formula-plugin.js.map +1 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +89 -0
- package/dist/plugin.js +99 -0
- package/dist/plugin.js.map +1 -0
- package/dist/query/filter-translator.d.ts +37 -0
- package/dist/query/filter-translator.js +135 -0
- package/dist/query/filter-translator.js.map +1 -0
- package/dist/query/index.d.ts +22 -0
- package/dist/query/index.js +39 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/query-analyzer.d.ts +186 -0
- package/dist/query/query-analyzer.js +349 -0
- package/dist/query/query-analyzer.js.map +1 -0
- package/dist/query/query-builder.d.ts +27 -0
- package/dist/query/query-builder.js +71 -0
- package/dist/query/query-builder.js.map +1 -0
- package/dist/query/query-service.d.ts +150 -0
- package/dist/query/query-service.js +268 -0
- package/dist/query/query-service.js.map +1 -0
- package/dist/repository.d.ts +23 -2
- package/dist/repository.js +62 -13
- package/dist/repository.js.map +1 -1
- package/dist/util.d.ts +7 -0
- package/dist/util.js +18 -3
- package/dist/util.js.map +1 -1
- package/dist/validator-plugin.d.ts +56 -0
- package/dist/validator-plugin.js +106 -0
- package/dist/validator-plugin.js.map +1 -0
- package/dist/validator.d.ts +7 -0
- package/dist/validator.js +10 -8
- package/dist/validator.js.map +1 -1
- package/jest.config.js +16 -0
- package/package.json +8 -5
- package/src/ai-agent.ts +8 -0
- package/src/app.ts +136 -72
- package/src/formula-engine.ts +8 -0
- package/src/formula-plugin.ts +141 -0
- package/src/index.ts +25 -3
- package/src/plugin.ts +179 -0
- package/src/query/filter-translator.ts +147 -0
- package/src/query/index.ts +24 -0
- package/src/query/query-analyzer.ts +535 -0
- package/src/query/query-builder.ts +80 -0
- package/src/query/query-service.ts +392 -0
- package/src/repository.ts +81 -17
- package/src/util.ts +19 -3
- package/src/validator-plugin.ts +140 -0
- package/src/validator.ts +12 -5
- package/test/__mocks__/@objectstack/runtime.ts +255 -0
- package/test/app.test.ts +23 -35
- package/test/filter-syntax.test.ts +233 -0
- package/test/formula-engine.test.ts +8 -0
- package/test/formula-integration.test.ts +8 -0
- package/test/formula-plugin.test.ts +197 -0
- package/test/introspection.test.ts +8 -0
- package/test/mock-driver.ts +8 -0
- package/test/plugin-integration.test.ts +213 -0
- package/test/repository-validation.test.ts +8 -0
- package/test/repository.test.ts +8 -0
- package/test/util.test.ts +9 -1
- package/test/utils.ts +8 -0
- package/test/validator-plugin.test.ts +126 -0
- package/test/validator.test.ts +8 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -23
- package/dist/action.js.map +0 -1
- package/dist/hook.d.ts +0 -8
- package/dist/hook.js +0 -25
- package/dist/hook.js.map +0 -1
- package/dist/object.d.ts +0 -3
- package/dist/object.js +0 -28
- package/dist/object.js.map +0 -1
- package/src/action.ts +0 -40
- package/src/hook.ts +0 -42
- package/src/object.ts +0 -26
- package/test/action.test.ts +0 -276
- package/test/hook.test.ts +0 -343
- package/test/object.test.ts +0 -183
package/src/util.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
import { ObjectConfig, FieldConfig, FieldType, IntrospectedSchema, IntrospectedColumn, IntrospectedTable } from '@objectql/types';
|
|
2
10
|
|
|
3
11
|
export function toTitleCase(str: string): string {
|
|
@@ -102,20 +110,28 @@ export function convertIntrospectedSchemaToObjects(
|
|
|
102
110
|
|
|
103
111
|
if (foreignKey) {
|
|
104
112
|
// This is a lookup field
|
|
113
|
+
// Note: name must be set explicitly here since we're creating the config programmatically.
|
|
114
|
+
// When defined in YAML (ObjectConfig.fields Record), the name is auto-populated from the key.
|
|
105
115
|
fieldConfig = {
|
|
116
|
+
name: column.name,
|
|
106
117
|
type: 'lookup',
|
|
107
118
|
reference_to: foreignKey.referencedTable,
|
|
108
119
|
label: toTitleCase(column.name),
|
|
109
|
-
required: !column.nullable
|
|
120
|
+
required: !column.nullable,
|
|
121
|
+
searchable: false
|
|
110
122
|
};
|
|
111
123
|
} else {
|
|
112
124
|
// Regular field
|
|
113
125
|
const fieldType = mapDatabaseTypeToFieldType(column.type);
|
|
114
126
|
|
|
127
|
+
// Note: name must be set explicitly here since we're creating the config programmatically.
|
|
128
|
+
// When defined in YAML (ObjectConfig.fields Record), the name is auto-populated from the key.
|
|
115
129
|
fieldConfig = {
|
|
130
|
+
name: column.name,
|
|
116
131
|
type: fieldType,
|
|
117
132
|
label: toTitleCase(column.name),
|
|
118
|
-
required: !column.nullable
|
|
133
|
+
required: !column.nullable,
|
|
134
|
+
searchable: false
|
|
119
135
|
};
|
|
120
136
|
|
|
121
137
|
// Add unique constraint
|
|
@@ -125,7 +141,7 @@ export function convertIntrospectedSchemaToObjects(
|
|
|
125
141
|
|
|
126
142
|
// Add max length for text fields
|
|
127
143
|
if (column.maxLength && (fieldType === 'text' || fieldType === 'textarea')) {
|
|
128
|
-
fieldConfig.
|
|
144
|
+
fieldConfig.maxLength = column.maxLength;
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
// Add default value
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectQL Validator Plugin
|
|
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 type { RuntimePlugin, RuntimeContext, ObjectStackKernel } from '@objectstack/runtime';
|
|
10
|
+
import { Validator, ValidatorOptions } from './validator';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extended ObjectStack Kernel with validator capability
|
|
14
|
+
*/
|
|
15
|
+
interface KernelWithValidator extends ObjectStackKernel {
|
|
16
|
+
validator?: Validator;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration for the Validator Plugin
|
|
21
|
+
*/
|
|
22
|
+
export interface ValidatorPluginConfig extends ValidatorOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Enable validation on queries
|
|
25
|
+
* @default true
|
|
26
|
+
*/
|
|
27
|
+
enableQueryValidation?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enable validation on mutations
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
enableMutationValidation?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validator Plugin
|
|
38
|
+
*
|
|
39
|
+
* Wraps the ObjectQL Validator engine as an ObjectStack plugin.
|
|
40
|
+
* Registers validation middleware hooks into the kernel lifecycle.
|
|
41
|
+
*/
|
|
42
|
+
export class ValidatorPlugin implements RuntimePlugin {
|
|
43
|
+
name = '@objectql/validator';
|
|
44
|
+
version = '4.0.0';
|
|
45
|
+
|
|
46
|
+
private validator: Validator;
|
|
47
|
+
private config: ValidatorPluginConfig;
|
|
48
|
+
|
|
49
|
+
constructor(config: ValidatorPluginConfig = {}) {
|
|
50
|
+
this.config = {
|
|
51
|
+
enableQueryValidation: true,
|
|
52
|
+
enableMutationValidation: true,
|
|
53
|
+
...config
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Initialize the validator with language options
|
|
57
|
+
this.validator = new Validator({
|
|
58
|
+
language: config.language,
|
|
59
|
+
languageFallback: config.languageFallback,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Install the plugin into the kernel
|
|
65
|
+
* Registers validation middleware for queries and mutations
|
|
66
|
+
*/
|
|
67
|
+
async install(ctx: RuntimeContext): Promise<void> {
|
|
68
|
+
const kernel = ctx.engine as KernelWithValidator;
|
|
69
|
+
|
|
70
|
+
console.log(`[${this.name}] Installing validator plugin...`);
|
|
71
|
+
|
|
72
|
+
// Make validator accessible from the kernel for direct usage
|
|
73
|
+
kernel.validator = this.validator;
|
|
74
|
+
|
|
75
|
+
// Register validation middleware for queries (if enabled)
|
|
76
|
+
if (this.config.enableQueryValidation !== false) {
|
|
77
|
+
this.registerQueryValidation(kernel);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Register validation middleware for mutations (if enabled)
|
|
81
|
+
if (this.config.enableMutationValidation !== false) {
|
|
82
|
+
this.registerMutationValidation(kernel);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`[${this.name}] Validator plugin installed`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Register query validation middleware
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
private registerQueryValidation(kernel: KernelWithValidator): void {
|
|
93
|
+
// Check if kernel supports middleware hooks
|
|
94
|
+
if (typeof (kernel as any).use === 'function') {
|
|
95
|
+
(kernel as any).use('beforeQuery', async (context: any) => {
|
|
96
|
+
// Query validation logic
|
|
97
|
+
// In a real implementation, this would validate query parameters
|
|
98
|
+
// For now, this is a placeholder that demonstrates the integration pattern
|
|
99
|
+
if (context.query && context.metadata?.validation_rules) {
|
|
100
|
+
// Validation would happen here
|
|
101
|
+
// const result = await this.validator.validate(
|
|
102
|
+
// context.metadata.validation_rules,
|
|
103
|
+
// { /* validation context */ }
|
|
104
|
+
// );
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register mutation validation middleware
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
private registerMutationValidation(kernel: KernelWithValidator): void {
|
|
115
|
+
// Check if kernel supports middleware hooks
|
|
116
|
+
if (typeof (kernel as any).use === 'function') {
|
|
117
|
+
(kernel as any).use('beforeMutation', async (context: any) => {
|
|
118
|
+
// Mutation validation logic
|
|
119
|
+
// This would validate data before create/update operations
|
|
120
|
+
if (context.data && context.metadata?.validation_rules) {
|
|
121
|
+
// Validation would happen here
|
|
122
|
+
// const result = await this.validator.validate(
|
|
123
|
+
// context.metadata.validation_rules,
|
|
124
|
+
// { /* validation context */ }
|
|
125
|
+
// );
|
|
126
|
+
// if (!result.valid) {
|
|
127
|
+
// throw new Error('Validation failed: ' + result.errors.map(e => e.message).join(', '));
|
|
128
|
+
// }
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the validator instance for direct access
|
|
136
|
+
*/
|
|
137
|
+
getValidator(): Validator {
|
|
138
|
+
return this.validator;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/validator.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Validation engine for ObjectQL.
|
|
3
11
|
* Executes validation rules based on metadata configuration.
|
|
@@ -182,11 +190,10 @@ export class Validator {
|
|
|
182
190
|
}
|
|
183
191
|
}
|
|
184
192
|
|
|
185
|
-
// Pattern validation
|
|
186
|
-
|
|
187
|
-
if (patternValue) {
|
|
193
|
+
// Pattern validation
|
|
194
|
+
if (validation.pattern) {
|
|
188
195
|
try {
|
|
189
|
-
const pattern = new RegExp(
|
|
196
|
+
const pattern = new RegExp(validation.pattern);
|
|
190
197
|
if (!pattern.test(String(value))) {
|
|
191
198
|
results.push({
|
|
192
199
|
rule: `${fieldName}_pattern`,
|
|
@@ -200,7 +207,7 @@ export class Validator {
|
|
|
200
207
|
results.push({
|
|
201
208
|
rule: `${fieldName}_pattern`,
|
|
202
209
|
valid: false,
|
|
203
|
-
message: `Invalid regex pattern: ${
|
|
210
|
+
message: `Invalid regex pattern: ${validation.pattern}`,
|
|
204
211
|
severity: 'error',
|
|
205
212
|
fields: [fieldName],
|
|
206
213
|
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for @objectstack/runtime
|
|
3
|
+
* This mock is needed because the npm package has issues with Jest
|
|
4
|
+
* and we want to focus on testing ObjectQL's logic, not the kernel integration.
|
|
5
|
+
*
|
|
6
|
+
* For now, this mock delegates to the legacy driver to maintain backward compatibility
|
|
7
|
+
* during the migration phase.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Simple mock implementations of runtime managers
|
|
11
|
+
class MockMetadataRegistry {
|
|
12
|
+
private store = new Map<string, Map<string, any>>();
|
|
13
|
+
|
|
14
|
+
register(type: string, item: any): void {
|
|
15
|
+
if (!this.store.has(type)) {
|
|
16
|
+
this.store.set(type, new Map());
|
|
17
|
+
}
|
|
18
|
+
const typeMap = this.store.get(type)!;
|
|
19
|
+
typeMap.set(item.id || item.name, item);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get<T = any>(type: string, id: string): T | undefined {
|
|
23
|
+
const typeMap = this.store.get(type);
|
|
24
|
+
const item = typeMap?.get(id);
|
|
25
|
+
return item?.content as T;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
list<T = any>(type: string): T[] {
|
|
29
|
+
const typeMap = this.store.get(type);
|
|
30
|
+
if (!typeMap) return [];
|
|
31
|
+
return Array.from(typeMap.values()).map(item => item.content as T);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
unregister(type: string, id: string): boolean {
|
|
35
|
+
const typeMap = this.store.get(type);
|
|
36
|
+
if (!typeMap) return false;
|
|
37
|
+
return typeMap.delete(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getTypes(): string[] {
|
|
41
|
+
return Array.from(this.store.keys());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
unregisterPackage(packageName: string): void {
|
|
45
|
+
// Simple implementation - in real runtime this would filter by package
|
|
46
|
+
for (const [type, typeMap] of this.store.entries()) {
|
|
47
|
+
const toDelete: string[] = [];
|
|
48
|
+
for (const [id, item] of typeMap.entries()) {
|
|
49
|
+
if (item.packageName === packageName || item.package === packageName) {
|
|
50
|
+
toDelete.push(id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
toDelete.forEach(id => typeMap.delete(id));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class MockHookManager {
|
|
59
|
+
private hooks = new Map<string, any[]>();
|
|
60
|
+
|
|
61
|
+
register(hookName: string, objectName: string, handler: any, packageName?: string): void {
|
|
62
|
+
if (!this.hooks.has(hookName)) {
|
|
63
|
+
this.hooks.set(hookName, []);
|
|
64
|
+
}
|
|
65
|
+
this.hooks.get(hookName)!.push({ objectName, handler, packageName });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async trigger(hookName: string, objectName: string, context: any): Promise<void> {
|
|
69
|
+
const handlers = this.hooks.get(hookName) || [];
|
|
70
|
+
for (const entry of handlers) {
|
|
71
|
+
// Match on wildcard '*' or specific object name
|
|
72
|
+
if (entry.objectName === '*' || entry.objectName === objectName) {
|
|
73
|
+
await entry.handler(context);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
removePackage(packageName: string): void {
|
|
79
|
+
for (const [hookName, handlers] of this.hooks.entries()) {
|
|
80
|
+
this.hooks.set(hookName, handlers.filter(h => h.packageName !== packageName));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clear(): void {
|
|
85
|
+
this.hooks.clear();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class MockActionManager {
|
|
90
|
+
private actions = new Map<string, any>();
|
|
91
|
+
|
|
92
|
+
register(objectName: string, actionName: string, handler: any, packageName?: string): void {
|
|
93
|
+
const key = `${objectName}.${actionName}`;
|
|
94
|
+
this.actions.set(key, { handler, packageName });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async execute(objectName: string, actionName: string, context: any): Promise<any> {
|
|
98
|
+
const key = `${objectName}.${actionName}`;
|
|
99
|
+
const action = this.actions.get(key);
|
|
100
|
+
if (!action) {
|
|
101
|
+
throw new Error(`Action ${actionName} not found for object ${objectName}`);
|
|
102
|
+
}
|
|
103
|
+
return await action.handler(context);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get(objectName: string, actionName: string): any {
|
|
107
|
+
const key = `${objectName}.${actionName}`;
|
|
108
|
+
return this.actions.get(key)?.handler;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
removePackage(packageName: string): void {
|
|
112
|
+
const toDelete: string[] = [];
|
|
113
|
+
for (const [key, action] of this.actions.entries()) {
|
|
114
|
+
if (action.packageName === packageName) {
|
|
115
|
+
toDelete.push(key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
toDelete.forEach(key => this.actions.delete(key));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
clear(): void {
|
|
122
|
+
this.actions.clear();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class ObjectStackKernel {
|
|
127
|
+
public ql: unknown = null;
|
|
128
|
+
public metadata: MockMetadataRegistry;
|
|
129
|
+
public hooks: MockHookManager;
|
|
130
|
+
public actions: MockActionManager;
|
|
131
|
+
|
|
132
|
+
private plugins: any[] = [];
|
|
133
|
+
private driver: any = null; // Will be set by the ObjectQL app
|
|
134
|
+
|
|
135
|
+
constructor(plugins: any[] = []) {
|
|
136
|
+
this.plugins = plugins;
|
|
137
|
+
this.metadata = new MockMetadataRegistry();
|
|
138
|
+
this.hooks = new MockHookManager();
|
|
139
|
+
this.actions = new MockActionManager();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Method to set the driver for delegation during migration
|
|
143
|
+
setDriver(driver: any): void {
|
|
144
|
+
this.driver = driver;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async start(): Promise<void> {
|
|
148
|
+
// Mock implementation that calls plugin lifecycle methods
|
|
149
|
+
for (const plugin of this.plugins) {
|
|
150
|
+
if (plugin.install) {
|
|
151
|
+
await plugin.install({ engine: this });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const plugin of this.plugins) {
|
|
155
|
+
if (plugin.onStart) {
|
|
156
|
+
await plugin.onStart({ engine: this });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async seed(): Promise<void> {
|
|
162
|
+
// Mock implementation
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async find(objectName: string, query: any): Promise<{ value: Record<string, any>[]; count: number }> {
|
|
166
|
+
// Delegate to driver during migration phase
|
|
167
|
+
if (this.driver) {
|
|
168
|
+
// Convert QueryAST back to UnifiedQuery format for driver
|
|
169
|
+
const unifiedQuery: any = {};
|
|
170
|
+
|
|
171
|
+
if (query.fields) {
|
|
172
|
+
unifiedQuery.fields = query.fields;
|
|
173
|
+
}
|
|
174
|
+
if (query.filters) {
|
|
175
|
+
unifiedQuery.filters = query.filters;
|
|
176
|
+
}
|
|
177
|
+
if (query.sort) {
|
|
178
|
+
unifiedQuery.sort = query.sort.map((s: any) => [s.field, s.order]);
|
|
179
|
+
}
|
|
180
|
+
if (query.top !== undefined) {
|
|
181
|
+
unifiedQuery.limit = query.top;
|
|
182
|
+
}
|
|
183
|
+
if (query.skip !== undefined) {
|
|
184
|
+
unifiedQuery.skip = query.skip;
|
|
185
|
+
}
|
|
186
|
+
if (query.aggregations) {
|
|
187
|
+
unifiedQuery.aggregate = query.aggregations.map((agg: any) => ({
|
|
188
|
+
func: agg.function,
|
|
189
|
+
field: agg.field,
|
|
190
|
+
alias: agg.alias
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
if (query.groupBy) {
|
|
194
|
+
unifiedQuery.groupBy = query.groupBy;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const results = await this.driver.find(objectName, unifiedQuery, {});
|
|
198
|
+
return { value: results, count: results.length };
|
|
199
|
+
}
|
|
200
|
+
return { value: [], count: 0 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async get(objectName: string, id: string): Promise<Record<string, any>> {
|
|
204
|
+
// Delegate to driver during migration phase
|
|
205
|
+
if (this.driver) {
|
|
206
|
+
return await this.driver.findOne(objectName, id, {}, {});
|
|
207
|
+
}
|
|
208
|
+
return {};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async create(objectName: string, data: any): Promise<Record<string, any>> {
|
|
212
|
+
// Delegate to driver during migration phase
|
|
213
|
+
if (this.driver) {
|
|
214
|
+
return await this.driver.create(objectName, data, {});
|
|
215
|
+
}
|
|
216
|
+
return data;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async update(objectName: string, id: string, data: any): Promise<Record<string, any>> {
|
|
220
|
+
// Delegate to driver during migration phase
|
|
221
|
+
if (this.driver) {
|
|
222
|
+
return await this.driver.update(objectName, id, data, {});
|
|
223
|
+
}
|
|
224
|
+
return data;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async delete(objectName: string, id: string): Promise<boolean> {
|
|
228
|
+
// Delegate to driver during migration phase
|
|
229
|
+
if (this.driver) {
|
|
230
|
+
await this.driver.delete(objectName, id, {});
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getMetadata(objectName: string): any {
|
|
237
|
+
return {};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
getView(objectName: string, viewType?: 'list' | 'form'): any {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export class ObjectStackRuntimeProtocol {}
|
|
246
|
+
|
|
247
|
+
export interface RuntimeContext {
|
|
248
|
+
engine: ObjectStackKernel;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface RuntimePlugin {
|
|
252
|
+
name: string;
|
|
253
|
+
install?: (ctx: RuntimeContext) => void | Promise<void>;
|
|
254
|
+
onStart?: (ctx: RuntimeContext) => void | Promise<void>;
|
|
255
|
+
}
|
package/test/app.test.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
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
|
+
|
|
1
9
|
import { ObjectQL } from '../src/app';
|
|
2
10
|
import { MockDriver } from './mock-driver';
|
|
3
|
-
import { ObjectConfig,
|
|
11
|
+
import { ObjectConfig, HookContext, ActionContext, Metadata } from '@objectql/types';
|
|
12
|
+
import type { PluginDefinition } from '@objectstack/spec';
|
|
4
13
|
|
|
5
14
|
const todoObject: ObjectConfig = {
|
|
6
15
|
name: 'todo',
|
|
@@ -57,9 +66,9 @@ describe('ObjectQL App', () => {
|
|
|
57
66
|
});
|
|
58
67
|
|
|
59
68
|
it('should accept plugin instances', () => {
|
|
60
|
-
const mockPlugin:
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
const mockPlugin: PluginDefinition = {
|
|
70
|
+
id: 'test-plugin',
|
|
71
|
+
onEnable: jest.fn()
|
|
63
72
|
};
|
|
64
73
|
const app = new ObjectQL({
|
|
65
74
|
datasources: {},
|
|
@@ -282,11 +291,13 @@ describe('ObjectQL App', () => {
|
|
|
282
291
|
});
|
|
283
292
|
|
|
284
293
|
describe('Plugin System', () => {
|
|
285
|
-
it('should initialize plugins on init', async () => {
|
|
286
|
-
const
|
|
287
|
-
const
|
|
294
|
+
it('should initialize runtime plugins on init', async () => {
|
|
295
|
+
const installFn = jest.fn();
|
|
296
|
+
const onStartFn = jest.fn();
|
|
297
|
+
const mockPlugin = {
|
|
288
298
|
name: 'test-plugin',
|
|
289
|
-
|
|
299
|
+
install: installFn,
|
|
300
|
+
onStart: onStartFn
|
|
290
301
|
};
|
|
291
302
|
|
|
292
303
|
const app = new ObjectQL({
|
|
@@ -295,43 +306,20 @@ describe('ObjectQL App', () => {
|
|
|
295
306
|
});
|
|
296
307
|
|
|
297
308
|
await app.init();
|
|
298
|
-
expect(
|
|
309
|
+
expect(installFn).toHaveBeenCalled();
|
|
310
|
+
expect(onStartFn).toHaveBeenCalled();
|
|
299
311
|
});
|
|
300
312
|
|
|
301
313
|
it('should use plugin method', () => {
|
|
302
|
-
const mockPlugin
|
|
314
|
+
const mockPlugin = {
|
|
303
315
|
name: 'test-plugin',
|
|
304
|
-
|
|
316
|
+
install: jest.fn()
|
|
305
317
|
};
|
|
306
318
|
|
|
307
319
|
const app = new ObjectQL({ datasources: {} });
|
|
308
320
|
app.use(mockPlugin);
|
|
309
321
|
expect(app).toBeDefined();
|
|
310
322
|
});
|
|
311
|
-
|
|
312
|
-
it('should provide package-scoped proxy for plugins', async () => {
|
|
313
|
-
let capturedApp: any;
|
|
314
|
-
const mockPlugin: ObjectQLPlugin = {
|
|
315
|
-
name: 'test-plugin',
|
|
316
|
-
setup: async (app) => {
|
|
317
|
-
capturedApp = app;
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
(mockPlugin as any)._packageName = 'test-package';
|
|
321
|
-
|
|
322
|
-
const app = new ObjectQL({
|
|
323
|
-
datasources: {},
|
|
324
|
-
plugins: [mockPlugin]
|
|
325
|
-
});
|
|
326
|
-
app.registerObject(todoObject);
|
|
327
|
-
|
|
328
|
-
await app.init();
|
|
329
|
-
|
|
330
|
-
// Test proxied methods
|
|
331
|
-
const handler = jest.fn();
|
|
332
|
-
capturedApp.on('beforeCreate', 'todo', handler);
|
|
333
|
-
capturedApp.registerAction('todo', 'test', handler);
|
|
334
|
-
});
|
|
335
323
|
});
|
|
336
324
|
|
|
337
325
|
describe('Initialization', () => {
|