@objectstack/objectql 0.3.2 → 0.4.1

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 CHANGED
@@ -1,5 +1,39 @@
1
1
  # @objectstack/objectql
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Version synchronization and dependency updates
8
+
9
+ - Synchronized plugin-msw version to 0.4.1
10
+ - Updated runtime peer dependency versions to ^0.4.1
11
+ - Fixed internal dependency version mismatches
12
+
13
+ - Updated dependencies
14
+ - @objectstack/spec@0.4.1
15
+
16
+ ## 0.4.0
17
+
18
+ ### Minor Changes
19
+
20
+ - Release version 0.4.0
21
+
22
+ ## 0.3.3
23
+
24
+ ### Patch Changes
25
+
26
+ - Workflow and configuration improvements
27
+
28
+ - Enhanced GitHub workflows for CI, release, and PR automation
29
+ - Added comprehensive prompt templates for different protocol areas
30
+ - Improved project documentation and automation guides
31
+ - Updated changeset configuration
32
+ - Added cursor rules for better development experience
33
+
34
+ - Updated dependencies
35
+ - @objectstack/spec@0.3.3
36
+
3
37
  ## 0.3.2
4
38
 
5
39
  ### Patch Changes
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # ObjectQL Engine
2
+
3
+ **ObjectQL** is a schema-driven, cross-datasource query engine for the [ObjectStack](https://github.com/steedos/objectstack) ecosystem. It acts as a virtual "Meta-Database" that unifies access to SQL, NoSQL, and API data sources under a single semantic layer.
4
+
5
+ ## Features
6
+
7
+ - **Protocol Agnostic**: Uses standard `ObjectSchema` and `QueryAST` from `@objectstack/spec`.
8
+ - **Cross-Datasource**: Routes queries to the correct driver (Postgres, MongoDB, Redis, etc.) based on Object definition.
9
+ - **Unified API**: Single `find`, `insert`, `update`, `delete` API regardless of the underlying storage.
10
+ - **Plugin System**: Load objects and logic via standard Manifests.
11
+ - **Middleware**: (Planned) Support for Hooks and Validators.
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { ObjectQL } from '@objectstack/objectql';
17
+ import { MemoryDriver } from '@objectstack/driver-memory'; // Example driver
18
+
19
+ async function main() {
20
+ // 1. Initialize Engine
21
+ const ql = new ObjectQL();
22
+
23
+ // 2. Register Drivers
24
+ const memDriver = new MemoryDriver({ name: 'default' });
25
+ ql.registerDriver(memDriver, true);
26
+
27
+ // 3. Load Schema (via Plugin/Manifest)
28
+ await ql.use({
29
+ name: 'my-app',
30
+ objects: [
31
+ {
32
+ name: 'todo',
33
+ fields: {
34
+ title: { type: 'text' },
35
+ completed: { type: 'boolean' }
36
+ },
37
+ datasource: 'default'
38
+ }
39
+ ]
40
+ });
41
+
42
+ await ql.init();
43
+
44
+ // 4. Execute Queries
45
+ // Insert
46
+ await ql.insert('todo', { title: 'Buy Milk', completed: false });
47
+
48
+ // Find (Simple)
49
+ const todos = await ql.find('todo', { completed: false });
50
+
51
+ // Find (Advanced AST)
52
+ const results = await ql.find('todo', {
53
+ where: {
54
+ title: { $contains: 'Milk' }
55
+ },
56
+ limit: 10,
57
+ orderBy: [{ field: 'title', order: 'desc' }]
58
+ });
59
+
60
+ console.log(results);
61
+ }
62
+ ```
63
+
64
+ ## Architecture
65
+
66
+ - **SchemaRegistry**: Central store for all metadata (Objects, Apps, Config).
67
+ - **DriverRegistry**: Manages connections to physical data sources.
68
+ - **QueryPlanner**: (Internal) Normalizes simplified queries into `QueryAST`.
69
+ - **Executor**: Routes AST to the correct driver.
70
+
71
+ ## Roadmap
72
+
73
+ - [x] Basic CRUD
74
+ - [x] Driver Routing
75
+ - [ ] Cross-Object Joins (Federation)
76
+ - [ ] Validation Layer (Zod)
77
+ - [ ] Access Control (ACL)
78
+ - [ ] Caching Layer
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { HookContext } from '@objectstack/spec/data';
1
2
  import { DriverInterface, DriverOptions } from '@objectstack/spec/system';
2
3
  export { SchemaRegistry } from './registry';
4
+ export type HookHandler = (context: HookContext) => Promise<void> | void;
3
5
  /**
4
6
  * Host Context provided to plugins
5
7
  */
@@ -14,16 +16,136 @@ export interface PluginContext {
14
16
  export declare class ObjectQL {
15
17
  private drivers;
16
18
  private defaultDriver;
19
+ private hooks;
17
20
  private hostContext;
18
21
  constructor(hostContext?: Record<string, any>);
19
22
  /**
20
23
  * Load and Register a Plugin
21
24
  */
22
25
  use(manifestPart: any, runtimePart?: any): Promise<void>;
26
+ /**
27
+ * Register a hook
28
+ * @param event The event name (e.g. 'beforeFind', 'afterInsert')
29
+ * @param handler The handler function
30
+ */
31
+ registerHook(event: string, handler: HookHandler): void;
32
+ private triggerHooks;
23
33
  /**
24
34
  * Register a new storage driver
25
35
  */
26
36
  registerDriver(driver: DriverInterface, isDefault?: boolean): void;
37
+ /**
38
+ * Helper to get object definition
39
+ */
40
+ getSchema(objectName: string): {
41
+ fields: Record<string, {
42
+ type: "number" | "boolean" | "code" | "date" | "text" | "textarea" | "email" | "url" | "phone" | "password" | "markdown" | "html" | "richtext" | "currency" | "percent" | "datetime" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "file" | "avatar" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "json" | "color" | "rating" | "slider" | "signature" | "qrcode" | "progress" | "tags" | "vector";
43
+ required: boolean;
44
+ searchable: boolean;
45
+ multiple: boolean;
46
+ unique: boolean;
47
+ deleteBehavior: "set_null" | "cascade" | "restrict";
48
+ hidden: boolean;
49
+ readonly: boolean;
50
+ encryption: boolean;
51
+ index: boolean;
52
+ externalId: boolean;
53
+ options?: {
54
+ value: string;
55
+ label: string;
56
+ color?: string | undefined;
57
+ default?: boolean | undefined;
58
+ }[] | undefined;
59
+ min?: number | undefined;
60
+ max?: number | undefined;
61
+ formula?: string | undefined;
62
+ label?: string | undefined;
63
+ precision?: number | undefined;
64
+ name?: string | undefined;
65
+ description?: string | undefined;
66
+ format?: string | undefined;
67
+ defaultValue?: any;
68
+ maxLength?: number | undefined;
69
+ minLength?: number | undefined;
70
+ scale?: number | undefined;
71
+ reference?: string | undefined;
72
+ referenceFilters?: string[] | undefined;
73
+ writeRequiresMasterRead?: boolean | undefined;
74
+ expression?: string | undefined;
75
+ summaryOperations?: {
76
+ object: string;
77
+ function: "count" | "sum" | "avg" | "min" | "max";
78
+ field: string;
79
+ } | undefined;
80
+ language?: string | undefined;
81
+ theme?: string | undefined;
82
+ lineNumbers?: boolean | undefined;
83
+ maxRating?: number | undefined;
84
+ allowHalf?: boolean | undefined;
85
+ displayMap?: boolean | undefined;
86
+ allowGeocoding?: boolean | undefined;
87
+ addressFormat?: "us" | "uk" | "international" | undefined;
88
+ colorFormat?: "hex" | "rgb" | "rgba" | "hsl" | undefined;
89
+ allowAlpha?: boolean | undefined;
90
+ presetColors?: string[] | undefined;
91
+ step?: number | undefined;
92
+ showValue?: boolean | undefined;
93
+ marks?: Record<string, string> | undefined;
94
+ barcodeFormat?: "qr" | "ean13" | "ean8" | "code128" | "code39" | "upca" | "upce" | undefined;
95
+ qrErrorCorrection?: "L" | "M" | "Q" | "H" | undefined;
96
+ displayValue?: boolean | undefined;
97
+ allowScanning?: boolean | undefined;
98
+ currencyConfig?: {
99
+ precision: number;
100
+ currencyMode: "dynamic" | "fixed";
101
+ defaultCurrency: string;
102
+ } | undefined;
103
+ vectorConfig?: {
104
+ dimensions: number;
105
+ distanceMetric: "cosine" | "euclidean" | "dotProduct" | "manhattan";
106
+ normalized: boolean;
107
+ indexed: boolean;
108
+ indexType?: "flat" | "hnsw" | "ivfflat" | undefined;
109
+ } | undefined;
110
+ }>;
111
+ name: string;
112
+ active: boolean;
113
+ isSystem: boolean;
114
+ abstract: boolean;
115
+ datasource: string;
116
+ tags?: string[] | undefined;
117
+ label?: string | undefined;
118
+ description?: string | undefined;
119
+ search?: {
120
+ fields: string[];
121
+ displayFields?: string[] | undefined;
122
+ filters?: string[] | undefined;
123
+ } | undefined;
124
+ pluralLabel?: string | undefined;
125
+ icon?: string | undefined;
126
+ tableName?: string | undefined;
127
+ indexes?: {
128
+ fields: string[];
129
+ type?: "hash" | "btree" | "gin" | "gist" | undefined;
130
+ name?: string | undefined;
131
+ unique?: boolean | undefined;
132
+ }[] | undefined;
133
+ validations?: any[] | undefined;
134
+ titleFormat?: string | undefined;
135
+ compactLayout?: string[] | undefined;
136
+ enable?: {
137
+ searchable: boolean;
138
+ trackHistory: boolean;
139
+ apiEnabled: boolean;
140
+ files: boolean;
141
+ feeds: boolean;
142
+ activities: boolean;
143
+ trash: boolean;
144
+ mru: boolean;
145
+ clone: boolean;
146
+ apiMethods?: ("update" | "delete" | "get" | "list" | "create" | "upsert" | "bulk" | "aggregate" | "history" | "search" | "restore" | "purge" | "import" | "export")[] | undefined;
147
+ } | undefined;
148
+ } | undefined;
27
149
  /**
28
150
  * Helper to get the target driver
29
151
  */
@@ -33,9 +155,10 @@ export declare class ObjectQL {
33
155
  */
34
156
  init(): Promise<void>;
35
157
  destroy(): Promise<void>;
36
- find(object: string, filters?: any, options?: DriverOptions): Promise<Record<string, any>[]>;
37
- insert(object: string, data: Record<string, any>, options?: DriverOptions): Promise<Record<string, any>>;
38
- update(object: string, id: string, data: Record<string, any>, options?: DriverOptions): Promise<Record<string, any>>;
39
- delete(object: string, id: string, options?: DriverOptions): Promise<boolean>;
158
+ find(object: string, query?: any, options?: DriverOptions): Promise<any>;
159
+ findOne(object: string, idOrQuery: string | any, options?: DriverOptions): Promise<Record<string, any> | null>;
160
+ insert(object: string, data: Record<string, any>, options?: DriverOptions): Promise<any>;
161
+ update(object: string, id: string | number, data: Record<string, any>, options?: DriverOptions): Promise<any>;
162
+ delete(object: string, id: string | number, options?: DriverOptions): Promise<any>;
40
163
  }
41
164
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,aAAa,EAAuB,MAAM,0BAA0B,CAAC;AAI/F,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAEhB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,aAAa,CAAuB;IAG5C,OAAO,CAAC,WAAW,CAA2B;gBAElC,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM;IAKjD;;OAEG;IACG,GAAG,CAAC,YAAY,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,GAAG;IA4D9C;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,SAAS,GAAE,OAAe;IAclE;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;IACG,IAAI;IAYJ,OAAO;IAUP,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,GAAQ,EAAE,OAAO,CAAC,EAAE,aAAa;IAmB/D,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;IAYzE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;IAMrF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa;CAKjE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAI1E,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAEhB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,aAAa,CAAuB;IAG5C,OAAO,CAAC,KAAK,CAKX;IAGF,OAAO,CAAC,WAAW,CAA2B;gBAElC,WAAW,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM;IAKjD;;OAEG;IACG,GAAG,CAAC,YAAY,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,GAAG;IA4D9C;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW;YAQlC,YAAY;IAQ1B;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,eAAe,EAAE,SAAS,GAAE,OAAe;IAclE;;OAEG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM;;;;;;;;;;;;;mBAyMi/T,CAAC;;;qBAA2E,CAAC;uBAAyC,CAAC;;eAA2D,CAAC;eAAiC,CAAC;mBAAqC,CAAC;iBAAmC,CAAC;qBAAuC,CAAC;gBAAkC,CAAC;uBAAyC,CAAC;kBAAoC,CAAC;wBAA0C,CAAC;qBAAwB,CAAC;qBAAuC,CAAC;iBAAmC,CAAC;qBAAuC,CAAC;4BAA8C,CAAC;mCAAuD,CAAC;sBAAyC,CAAC;6BAA+C,CAAC;;;;;oBAAiK,CAAC;iBAAmC,CAAC;uBAAyC,CAAC;qBAAwC,CAAC;qBAAuC,CAAC;sBAAyC,CAAC;0BAA6C,CAAC;yBAA4C,CAAC;uBAAgE,CAAC;sBAAgE,CAAC;wBAA2C,CAAC;gBAAoC,CAAC;qBAAuC,CAAC;iBAAoC,CAAC;yBAA2D,CAAC;6BAAyG,CAAC;wBAAyD,CAAC;yBAA4C,CAAC;0BAA6C,CAAC;;;;;wBAAkK,CAAC;;;;;yBAAyM,CAAC;;;;;;;;;;;;;yBAA4V,CAAC;mBAAuC,CAAC;;;;;;;gBAA0M,CAAC;gBAA6D,CAAC;kBAAoC,CAAC;;;;;;;;;;;;;;;sBAA8a,CAAC;;;IArM/ya;;OAEG;IACH,OAAO,CAAC,SAAS;IAqCjB;;OAEG;IACG,IAAI;IAYJ,OAAO;IAUP,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,GAAQ,EAAE,OAAO,CAAC,EAAE,aAAa;IAqC7D,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE,OAAO,CAAC,EAAE,aAAa;IAwBxE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;IA+BzE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa;IAoB9F,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa;CAmB1E"}
package/dist/index.js CHANGED
@@ -8,6 +8,13 @@ export class ObjectQL {
8
8
  constructor(hostContext = {}) {
9
9
  this.drivers = new Map();
10
10
  this.defaultDriver = null;
11
+ // Hooks Registry
12
+ this.hooks = {
13
+ 'beforeFind': [], 'afterFind': [],
14
+ 'beforeInsert': [], 'afterInsert': [],
15
+ 'beforeUpdate': [], 'afterUpdate': [],
16
+ 'beforeDelete': [], 'afterDelete': [],
17
+ };
11
18
  // Host provided context additions (e.g. Server router)
12
19
  this.hostContext = {};
13
20
  this.hostContext = hostContext;
@@ -68,6 +75,25 @@ export class ObjectQL {
68
75
  }
69
76
  }
70
77
  }
78
+ /**
79
+ * Register a hook
80
+ * @param event The event name (e.g. 'beforeFind', 'afterInsert')
81
+ * @param handler The handler function
82
+ */
83
+ registerHook(event, handler) {
84
+ if (!this.hooks[event]) {
85
+ this.hooks[event] = [];
86
+ }
87
+ this.hooks[event].push(handler);
88
+ console.log(`[ObjectQL] Registered hook for ${event}`);
89
+ }
90
+ async triggerHooks(event, context) {
91
+ const handlers = this.hooks[event] || [];
92
+ for (const handler of handlers) {
93
+ // In a real system, we might want to catch errors here or allow them to bubble up
94
+ await handler(context);
95
+ }
96
+ }
71
97
  /**
72
98
  * Register a new storage driver
73
99
  */
@@ -82,16 +108,47 @@ export class ObjectQL {
82
108
  this.defaultDriver = driver.name;
83
109
  }
84
110
  }
111
+ /**
112
+ * Helper to get object definition
113
+ */
114
+ getSchema(objectName) {
115
+ return SchemaRegistry.getObject(objectName);
116
+ }
85
117
  /**
86
118
  * Helper to get the target driver
87
119
  */
88
- getDriver(_object) {
89
- // TODO: Look up Object definition to see if it specifies a specific datasource/driver
90
- // For now, always return default
91
- if (!this.defaultDriver) {
92
- throw new Error('[ObjectQL] No drivers registered!');
120
+ getDriver(objectName) {
121
+ const object = SchemaRegistry.getObject(objectName);
122
+ // 1. If object definition exists, check for explicit datasource
123
+ if (object) {
124
+ const datasourceName = object.datasource || 'default';
125
+ // If configured for 'default', try to find the default driver
126
+ if (datasourceName === 'default') {
127
+ if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
128
+ return this.drivers.get(this.defaultDriver);
129
+ }
130
+ // Fallback: If 'default' not explicitly set, use the first available driver?
131
+ // Better to be strict.
132
+ }
133
+ else {
134
+ // Specific datasource requested
135
+ if (this.drivers.has(datasourceName)) {
136
+ return this.drivers.get(datasourceName);
137
+ }
138
+ // If not found, fall back to default? Or error?
139
+ // Standard behavior: Error if specific datasource is missing.
140
+ throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
141
+ }
142
+ }
143
+ // 2. Fallback for ad-hoc objects or missing definitions
144
+ // If we have a default driver, use it.
145
+ if (this.defaultDriver) {
146
+ if (!object) {
147
+ console.warn(`[ObjectQL] Object '${objectName}' not found in registry. Using default driver.`);
148
+ }
149
+ return this.drivers.get(this.defaultDriver);
93
150
  }
94
- return this.drivers.get(this.defaultDriver);
151
+ throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
95
152
  }
96
153
  /**
97
154
  * Initialize the engine and all registered drivers
@@ -116,39 +173,114 @@ export class ObjectQL {
116
173
  // ============================================
117
174
  // Data Access Methods
118
175
  // ============================================
119
- async find(object, filters = {}, options) {
176
+ async find(object, query = {}, options) {
120
177
  const driver = this.getDriver(object);
121
- console.log(`[ObjectQL] Finding ${object} on ${driver.name}...`);
122
- // Transform simplified filters to QueryAST
123
- // This is a simplified "Mock" transform.
124
- // Real implementation would parse complex JSON or FilterBuilders.
125
- const ast = {
126
- object, // Add missing required field
127
- // Pass through if it looks like AST, otherwise empty
128
- // In this demo, we assume the caller passes a simplified object or raw AST
129
- filters: filters.filters || undefined,
130
- top: filters.top || 100,
131
- sort: filters.sort || []
178
+ // Normalize QueryAST
179
+ let ast;
180
+ if (query.where || query.fields || query.orderBy || query.limit) {
181
+ ast = { object, ...query };
182
+ }
183
+ else {
184
+ ast = { object, where: query };
185
+ }
186
+ if (ast.limit === undefined)
187
+ ast.limit = 100;
188
+ // Trigger Before Hook
189
+ const hookContext = {
190
+ object,
191
+ event: 'beforeFind',
192
+ input: { ast, options }, // Hooks can modify AST here
193
+ ql: this
132
194
  };
133
- return driver.find(object, ast, options);
195
+ await this.triggerHooks('beforeFind', hookContext);
196
+ try {
197
+ const result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
198
+ // Trigger After Hook
199
+ hookContext.event = 'afterFind';
200
+ hookContext.result = result;
201
+ await this.triggerHooks('afterFind', hookContext);
202
+ return hookContext.result;
203
+ }
204
+ catch (e) {
205
+ // hookContext.error = e;
206
+ throw e;
207
+ }
208
+ }
209
+ async findOne(object, idOrQuery, options) {
210
+ const driver = this.getDriver(object);
211
+ let ast;
212
+ if (typeof idOrQuery === 'string') {
213
+ ast = {
214
+ object,
215
+ where: { _id: idOrQuery }
216
+ };
217
+ }
218
+ else {
219
+ // Assume query object
220
+ // reuse logic from find() or just wrap it
221
+ if (idOrQuery.where || idOrQuery.fields) {
222
+ ast = { object, ...idOrQuery };
223
+ }
224
+ else {
225
+ ast = { object, where: idOrQuery };
226
+ }
227
+ }
228
+ // Limit 1 for findOne
229
+ ast.limit = 1;
230
+ return driver.findOne(object, ast, options);
134
231
  }
135
232
  async insert(object, data, options) {
136
233
  const driver = this.getDriver(object);
137
- console.log(`[ObjectQL] Creating ${object} on ${driver.name}...`);
138
- // 1. Validate Schema
139
- // 2. Run "Before Insert" Triggers
140
- const result = await driver.create(object, data, options);
141
- // 3. Run "After Insert" Triggers
142
- return result;
234
+ // 1. Get Schema
235
+ const schema = SchemaRegistry.getObject(object);
236
+ if (schema) {
237
+ // TODO: Validation Logic
238
+ // validate(schema, data);
239
+ }
240
+ // 2. Trigger Before Hook
241
+ const hookContext = {
242
+ object,
243
+ event: 'beforeInsert',
244
+ input: { data, options },
245
+ ql: this
246
+ };
247
+ await this.triggerHooks('beforeInsert', hookContext);
248
+ // 3. Execute Driver
249
+ const result = await driver.create(object, hookContext.input.data, hookContext.input.options);
250
+ // 4. Trigger After Hook
251
+ hookContext.event = 'afterInsert';
252
+ hookContext.result = result;
253
+ await this.triggerHooks('afterInsert', hookContext);
254
+ return hookContext.result;
143
255
  }
144
256
  async update(object, id, data, options) {
145
257
  const driver = this.getDriver(object);
146
- console.log(`[ObjectQL] Updating ${object} ${id}...`);
147
- return driver.update(object, id, data, options);
258
+ const hookContext = {
259
+ object,
260
+ event: 'beforeUpdate',
261
+ input: { id, data, options },
262
+ ql: this
263
+ };
264
+ await this.triggerHooks('beforeUpdate', hookContext);
265
+ const result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
266
+ hookContext.event = 'afterUpdate';
267
+ hookContext.result = result;
268
+ await this.triggerHooks('afterUpdate', hookContext);
269
+ return hookContext.result;
148
270
  }
149
271
  async delete(object, id, options) {
150
272
  const driver = this.getDriver(object);
151
- console.log(`[ObjectQL] Deleting ${object} ${id}...`);
152
- return driver.delete(object, id, options);
273
+ const hookContext = {
274
+ object,
275
+ event: 'beforeDelete',
276
+ input: { id, options },
277
+ ql: this
278
+ };
279
+ await this.triggerHooks('beforeDelete', hookContext);
280
+ const result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
281
+ hookContext.event = 'afterDelete';
282
+ hookContext.result = result;
283
+ await this.triggerHooks('afterDelete', hookContext);
284
+ return hookContext.result;
153
285
  }
154
286
  }
@@ -1,5 +1,4 @@
1
1
  import { ServiceObject } from '@objectstack/spec/data';
2
- import { App } from '@objectstack/spec/ui';
3
2
  import { ObjectStackManifest } from '@objectstack/spec/system';
4
3
  /**
5
4
  * Global Schema Registry
@@ -32,12 +31,6 @@ export declare class SchemaRegistry {
32
31
  static registerObject(schema: ServiceObject): void;
33
32
  static getObject(name: string): ServiceObject | undefined;
34
33
  static getAllObjects(): ServiceObject[];
35
- /**
36
- * App Helpers
37
- */
38
- static registerApp(app: App): void;
39
- static getApp(name: string): App | undefined;
40
- static getAllApps(): App[];
41
34
  /**
42
35
  * Plugin Helpers
43
36
  */
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;GAGG;AACH,qBAAa,cAAc;IAEzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAuC;IAE9D;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,GAAE,MAAM,CAAqB;IAcnF;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI5D;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE;IAItC;;OAEG;IACH,MAAM,CAAC,kBAAkB,IAAI,MAAM,EAAE;IAQrC;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,aAAa;IAI3C,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIzD,MAAM,CAAC,aAAa,IAAI,aAAa,EAAE;IAIvC;;OAEG;IACH,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG;IAI3B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAI5C,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE;IAI1B;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,mBAAmB;IAInD,MAAM,CAAC,aAAa,IAAI,mBAAmB,EAAE;IAI7C;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE;IAIzD,MAAM,CAAC,WAAW,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE;CAGxD"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;GAGG;AACH,qBAAa,cAAc;IAEzB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAuC;IAE9D;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,GAAE,MAAM,CAAqB;IAcnF;;OAEG;IACH,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI5D;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE;IAItC;;OAEG;IACH,MAAM,CAAC,kBAAkB,IAAI,MAAM,EAAE;IAQrC;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,aAAa;IAI3C,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAIzD,MAAM,CAAC,aAAa,IAAI,aAAa,EAAE;IAIvC;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,mBAAmB;IAInD,MAAM,CAAC,aAAa,IAAI,mBAAmB,EAAE;IAI7C;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE;IAIzD,MAAM,CAAC,WAAW,IAAI;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE;CAGxD"}
package/dist/registry.js CHANGED
@@ -54,18 +54,6 @@ export class SchemaRegistry {
54
54
  static getAllObjects() {
55
55
  return this.listItems('object');
56
56
  }
57
- /**
58
- * App Helpers
59
- */
60
- static registerApp(app) {
61
- this.registerItem('app', app, 'name');
62
- }
63
- static getApp(name) {
64
- return this.getItem('app', name);
65
- }
66
- static getAllApps() {
67
- return this.listItems('app');
68
- }
69
57
  /**
70
58
  * Plugin Helpers
71
59
  */
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@objectstack/objectql",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Isomorphic ObjectQL Engine for ObjectStack",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
7
  "dependencies": {
8
- "@objectstack/spec": "0.3.2"
8
+ "@objectstack/spec": "0.4.1"
9
9
  },
10
10
  "devDependencies": {
11
11
  "typescript": "^5.0.0",
package/src/index.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { QueryAST } from '@objectstack/spec/data';
2
- import { DriverInterface, DriverOptions, ObjectStackManifest } from '@objectstack/spec/system';
1
+ import { QueryAST, HookContext } from '@objectstack/spec/data';
2
+ import { ObjectStackManifest } from '@objectstack/spec/system';
3
+ import { DriverInterface, DriverOptions } from '@objectstack/spec/system';
3
4
  import { SchemaRegistry } from './registry';
4
5
 
5
6
  // Export Registry for consumers
6
7
  export { SchemaRegistry } from './registry';
7
8
 
9
+ export type HookHandler = (context: HookContext) => Promise<void> | void;
10
+
8
11
  /**
9
12
  * Host Context provided to plugins
10
13
  */
@@ -22,6 +25,14 @@ export class ObjectQL {
22
25
  private drivers = new Map<string, DriverInterface>();
23
26
  private defaultDriver: string | null = null;
24
27
 
28
+ // Hooks Registry
29
+ private hooks: Record<string, HookHandler[]> = {
30
+ 'beforeFind': [], 'afterFind': [],
31
+ 'beforeInsert': [], 'afterInsert': [],
32
+ 'beforeUpdate': [], 'afterUpdate': [],
33
+ 'beforeDelete': [], 'afterDelete': [],
34
+ };
35
+
25
36
  // Host provided context additions (e.g. Server router)
26
37
  private hostContext: Record<string, any> = {};
27
38
 
@@ -93,6 +104,27 @@ export class ObjectQL {
93
104
  }
94
105
  }
95
106
 
107
+ /**
108
+ * Register a hook
109
+ * @param event The event name (e.g. 'beforeFind', 'afterInsert')
110
+ * @param handler The handler function
111
+ */
112
+ registerHook(event: string, handler: HookHandler) {
113
+ if (!this.hooks[event]) {
114
+ this.hooks[event] = [];
115
+ }
116
+ this.hooks[event].push(handler);
117
+ console.log(`[ObjectQL] Registered hook for ${event}`);
118
+ }
119
+
120
+ private async triggerHooks(event: string, context: HookContext) {
121
+ const handlers = this.hooks[event] || [];
122
+ for (const handler of handlers) {
123
+ // In a real system, we might want to catch errors here or allow them to bubble up
124
+ await handler(context);
125
+ }
126
+ }
127
+
96
128
  /**
97
129
  * Register a new storage driver
98
130
  */
@@ -110,16 +142,51 @@ export class ObjectQL {
110
142
  }
111
143
  }
112
144
 
145
+ /**
146
+ * Helper to get object definition
147
+ */
148
+ getSchema(objectName: string) {
149
+ return SchemaRegistry.getObject(objectName);
150
+ }
151
+
113
152
  /**
114
153
  * Helper to get the target driver
115
154
  */
116
- private getDriver(_object: string): DriverInterface {
117
- // TODO: Look up Object definition to see if it specifies a specific datasource/driver
118
- // For now, always return default
119
- if (!this.defaultDriver) {
120
- throw new Error('[ObjectQL] No drivers registered!');
155
+ private getDriver(objectName: string): DriverInterface {
156
+ const object = SchemaRegistry.getObject(objectName);
157
+
158
+ // 1. If object definition exists, check for explicit datasource
159
+ if (object) {
160
+ const datasourceName = object.datasource || 'default';
161
+
162
+ // If configured for 'default', try to find the default driver
163
+ if (datasourceName === 'default') {
164
+ if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
165
+ return this.drivers.get(this.defaultDriver)!;
166
+ }
167
+ // Fallback: If 'default' not explicitly set, use the first available driver?
168
+ // Better to be strict.
169
+ } else {
170
+ // Specific datasource requested
171
+ if (this.drivers.has(datasourceName)) {
172
+ return this.drivers.get(datasourceName)!;
173
+ }
174
+ // If not found, fall back to default? Or error?
175
+ // Standard behavior: Error if specific datasource is missing.
176
+ throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
177
+ }
121
178
  }
122
- return this.drivers.get(this.defaultDriver)!;
179
+
180
+ // 2. Fallback for ad-hoc objects or missing definitions
181
+ // If we have a default driver, use it.
182
+ if (this.defaultDriver) {
183
+ if (!object) {
184
+ console.warn(`[ObjectQL] Object '${objectName}' not found in registry. Using default driver.`);
185
+ }
186
+ return this.drivers.get(this.defaultDriver)!;
187
+ }
188
+
189
+ throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
123
190
  }
124
191
 
125
192
  /**
@@ -147,46 +214,135 @@ export class ObjectQL {
147
214
  // Data Access Methods
148
215
  // ============================================
149
216
 
150
- async find(object: string, filters: any = {}, options?: DriverOptions) {
217
+ async find(object: string, query: any = {}, options?: DriverOptions) {
151
218
  const driver = this.getDriver(object);
152
- console.log(`[ObjectQL] Finding ${object} on ${driver.name}...`);
153
219
 
154
- // Transform simplified filters to QueryAST
155
- // This is a simplified "Mock" transform.
156
- // Real implementation would parse complex JSON or FilterBuilders.
157
- const ast: QueryAST = {
158
- object, // Add missing required field
159
- // Pass through if it looks like AST, otherwise empty
160
- // In this demo, we assume the caller passes a simplified object or raw AST
161
- filters: filters.filters || undefined,
162
- top: filters.top || 100,
163
- sort: filters.sort || []
220
+ // Normalize QueryAST
221
+ let ast: QueryAST;
222
+ if (query.where || query.fields || query.orderBy || query.limit) {
223
+ ast = { object, ...query } as QueryAST;
224
+ } else {
225
+ ast = { object, where: query } as QueryAST;
226
+ }
227
+
228
+ if (ast.limit === undefined) ast.limit = 100;
229
+
230
+ // Trigger Before Hook
231
+ const hookContext: HookContext = {
232
+ object,
233
+ event: 'beforeFind',
234
+ input: { ast, options }, // Hooks can modify AST here
235
+ ql: this
164
236
  };
237
+ await this.triggerHooks('beforeFind', hookContext);
165
238
 
166
- return driver.find(object, ast, options);
239
+ try {
240
+ const result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
241
+
242
+ // Trigger After Hook
243
+ hookContext.event = 'afterFind';
244
+ hookContext.result = result;
245
+ await this.triggerHooks('afterFind', hookContext);
246
+
247
+ return hookContext.result;
248
+ } catch (e) {
249
+ // hookContext.error = e;
250
+ throw e;
251
+ }
252
+ }
253
+
254
+ async findOne(object: string, idOrQuery: string | any, options?: DriverOptions) {
255
+ const driver = this.getDriver(object);
256
+
257
+ let ast: QueryAST;
258
+ if (typeof idOrQuery === 'string') {
259
+ ast = {
260
+ object,
261
+ where: { _id: idOrQuery }
262
+ };
263
+ } else {
264
+ // Assume query object
265
+ // reuse logic from find() or just wrap it
266
+ if (idOrQuery.where || idOrQuery.fields) {
267
+ ast = { object, ...idOrQuery };
268
+ } else {
269
+ ast = { object, where: idOrQuery };
270
+ }
271
+ }
272
+ // Limit 1 for findOne
273
+ ast.limit = 1;
274
+
275
+ return driver.findOne(object, ast, options);
167
276
  }
168
277
 
169
278
  async insert(object: string, data: Record<string, any>, options?: DriverOptions) {
170
279
  const driver = this.getDriver(object);
171
- console.log(`[ObjectQL] Creating ${object} on ${driver.name}...`);
172
- // 1. Validate Schema
173
- // 2. Run "Before Insert" Triggers
174
280
 
175
- const result = await driver.create(object, data, options);
281
+ // 1. Get Schema
282
+ const schema = SchemaRegistry.getObject(object);
283
+
284
+ if (schema) {
285
+ // TODO: Validation Logic
286
+ // validate(schema, data);
287
+ }
288
+
289
+ // 2. Trigger Before Hook
290
+ const hookContext: HookContext = {
291
+ object,
292
+ event: 'beforeInsert',
293
+ input: { data, options },
294
+ ql: this
295
+ };
296
+ await this.triggerHooks('beforeInsert', hookContext);
176
297
 
177
- // 3. Run "After Insert" Triggers
178
- return result;
298
+ // 3. Execute Driver
299
+ const result = await driver.create(object, hookContext.input.data, hookContext.input.options);
300
+
301
+ // 4. Trigger After Hook
302
+ hookContext.event = 'afterInsert';
303
+ hookContext.result = result;
304
+ await this.triggerHooks('afterInsert', hookContext);
305
+
306
+ return hookContext.result;
179
307
  }
180
308
 
181
- async update(object: string, id: string, data: Record<string, any>, options?: DriverOptions) {
309
+ async update(object: string, id: string | number, data: Record<string, any>, options?: DriverOptions) {
182
310
  const driver = this.getDriver(object);
183
- console.log(`[ObjectQL] Updating ${object} ${id}...`);
184
- return driver.update(object, id, data, options);
311
+
312
+ const hookContext: HookContext = {
313
+ object,
314
+ event: 'beforeUpdate',
315
+ input: { id, data, options },
316
+ ql: this
317
+ };
318
+ await this.triggerHooks('beforeUpdate', hookContext);
319
+
320
+ const result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
321
+
322
+ hookContext.event = 'afterUpdate';
323
+ hookContext.result = result;
324
+ await this.triggerHooks('afterUpdate', hookContext);
325
+
326
+ return hookContext.result;
185
327
  }
186
328
 
187
- async delete(object: string, id: string, options?: DriverOptions) {
329
+ async delete(object: string, id: string | number, options?: DriverOptions) {
188
330
  const driver = this.getDriver(object);
189
- console.log(`[ObjectQL] Deleting ${object} ${id}...`);
190
- return driver.delete(object, id, options);
331
+
332
+ const hookContext: HookContext = {
333
+ object,
334
+ event: 'beforeDelete',
335
+ input: { id, options },
336
+ ql: this
337
+ };
338
+ await this.triggerHooks('beforeDelete', hookContext);
339
+
340
+ const result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
341
+
342
+ hookContext.event = 'afterDelete';
343
+ hookContext.result = result;
344
+ await this.triggerHooks('afterDelete', hookContext);
345
+
346
+ return hookContext.result;
191
347
  }
192
348
  }
package/src/registry.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { ServiceObject } from '@objectstack/spec/data';
2
- import { App } from '@objectstack/spec/ui';
3
2
  import { ObjectStackManifest } from '@objectstack/spec/system';
4
3
 
5
4
  /**
@@ -70,21 +69,6 @@ export class SchemaRegistry {
70
69
  return this.listItems<ServiceObject>('object');
71
70
  }
72
71
 
73
- /**
74
- * App Helpers
75
- */
76
- static registerApp(app: App) {
77
- this.registerItem('app', app, 'name');
78
- }
79
-
80
- static getApp(name: string): App | undefined {
81
- return this.getItem<App>('app', name);
82
- }
83
-
84
- static getAllApps(): App[] {
85
- return this.listItems<App>('app');
86
- }
87
-
88
72
  /**
89
73
  * Plugin Helpers
90
74
  */