@objectstack/objectql 4.0.2 → 4.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/objectql",
3
- "version": "4.0.2",
3
+ "version": "4.0.4",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Isomorphic ObjectQL Engine for ObjectStack",
6
6
  "main": "dist/index.js",
@@ -13,13 +13,13 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "4.0.2",
17
- "@objectstack/spec": "4.0.2",
18
- "@objectstack/types": "4.0.2"
16
+ "@objectstack/core": "4.0.4",
17
+ "@objectstack/spec": "4.0.4",
18
+ "@objectstack/types": "4.0.4"
19
19
  },
20
20
  "devDependencies": {
21
21
  "typescript": "^6.0.2",
22
- "vitest": "^4.1.2"
22
+ "vitest": "^4.1.4"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsup --config ../../tsup.config.ts",
@@ -0,0 +1,181 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import { describe, it, expect, beforeEach } from 'vitest';
4
+ import { ObjectQL } from './engine.js';
5
+ import { SchemaRegistry } from './registry.js';
6
+
7
+ // Mock driver for testing
8
+ const createMockDriver = (name: string) => ({
9
+ name,
10
+ version: '1.0.0',
11
+ supports: {},
12
+ connect: async () => {},
13
+ disconnect: async () => {},
14
+ checkHealth: async () => true,
15
+ find: async () => [],
16
+ findOne: async () => null,
17
+ create: async (obj: string, data: any) => ({ id: '1', ...data }),
18
+ update: async (obj: string, id: string, data: any) => ({ id, ...data }),
19
+ delete: async () => true,
20
+ count: async () => 0,
21
+ bulkCreate: async () => [],
22
+ bulkUpdate: async () => [],
23
+ bulkDelete: async () => {},
24
+ execute: async () => ({}),
25
+ findStream: async function* () {},
26
+ upsert: async (obj: string, data: any) => ({ id: '1', ...data }),
27
+ beginTransaction: async () => ({}),
28
+ commit: async () => {},
29
+ rollback: async () => {},
30
+ syncSchema: async () => {},
31
+ });
32
+
33
+ describe('DatasourceMapping', () => {
34
+ let engine: ObjectQL;
35
+
36
+ beforeEach(() => {
37
+ engine = new ObjectQL();
38
+ SchemaRegistry.reset();
39
+ });
40
+
41
+ it('should route objects by namespace', async () => {
42
+ const memoryDriver = createMockDriver('memory');
43
+ const tursoDriver = createMockDriver('turso');
44
+
45
+ engine.registerDriver(memoryDriver);
46
+ engine.registerDriver(tursoDriver, true); // default
47
+
48
+ // Configure mapping: crm namespace → memory
49
+ engine.setDatasourceMapping([
50
+ { namespace: 'crm', datasource: 'memory' },
51
+ ]);
52
+
53
+ // Register an object in crm namespace
54
+ SchemaRegistry.registerObject(
55
+ {
56
+ name: 'account',
57
+ fields: { name: { type: 'text' } },
58
+ },
59
+ 'com.example.crm',
60
+ 'crm',
61
+ 'own'
62
+ );
63
+
64
+ // Test that it uses memory driver
65
+ const result = await engine.insert('account', { name: 'Test Account' });
66
+ expect(result).toBeDefined();
67
+ expect(result.name).toBe('Test Account');
68
+ });
69
+
70
+ it('should route objects by pattern', async () => {
71
+ const memoryDriver = createMockDriver('memory');
72
+ const tursoDriver = createMockDriver('turso');
73
+
74
+ engine.registerDriver(memoryDriver);
75
+ engine.registerDriver(tursoDriver, true);
76
+
77
+ // Configure mapping: sys_* pattern → turso
78
+ engine.setDatasourceMapping([
79
+ { objectPattern: 'sys_*', datasource: 'turso' },
80
+ { default: true, datasource: 'memory' },
81
+ ]);
82
+
83
+ // Register system objects
84
+ SchemaRegistry.registerObject(
85
+ {
86
+ name: 'sys_user',
87
+ fields: { username: { type: 'text' } },
88
+ },
89
+ 'com.objectstack.system',
90
+ 'system',
91
+ 'own'
92
+ );
93
+
94
+ const result = await engine.insert('sys_user', { username: 'admin' });
95
+ expect(result).toBeDefined();
96
+ });
97
+
98
+ it('should respect priority order', async () => {
99
+ const memoryDriver = createMockDriver('memory');
100
+ const tursoDriver = createMockDriver('turso');
101
+
102
+ engine.registerDriver(memoryDriver);
103
+ engine.registerDriver(tursoDriver);
104
+
105
+ // Higher priority rule should win
106
+ engine.setDatasourceMapping([
107
+ { namespace: 'crm', datasource: 'memory', priority: 100 },
108
+ { namespace: 'crm', datasource: 'turso', priority: 50 }, // Lower number = higher priority
109
+ ]);
110
+
111
+ SchemaRegistry.registerObject(
112
+ {
113
+ name: 'account',
114
+ fields: { name: { type: 'text' } },
115
+ },
116
+ 'com.example.crm',
117
+ 'crm',
118
+ 'own'
119
+ );
120
+
121
+ // Should use turso (priority 50) not memory (priority 100)
122
+ const result = await engine.insert('account', { name: 'Test' });
123
+ expect(result).toBeDefined();
124
+ });
125
+
126
+ it('should fallback to default rule', async () => {
127
+ const memoryDriver = createMockDriver('memory');
128
+ const tursoDriver = createMockDriver('turso');
129
+
130
+ engine.registerDriver(memoryDriver);
131
+ engine.registerDriver(tursoDriver);
132
+
133
+ engine.setDatasourceMapping([
134
+ { namespace: 'auth', datasource: 'turso' },
135
+ { default: true, datasource: 'memory' },
136
+ ]);
137
+
138
+ // Register object in different namespace
139
+ SchemaRegistry.registerObject(
140
+ {
141
+ name: 'task',
142
+ fields: { title: { type: 'text' } },
143
+ },
144
+ 'com.example.todo',
145
+ 'todo',
146
+ 'own'
147
+ );
148
+
149
+ // Should use memory (default)
150
+ const result = await engine.insert('task', { title: 'Do something' });
151
+ expect(result).toBeDefined();
152
+ });
153
+
154
+ it('should prefer object explicit datasource over mapping', async () => {
155
+ const memoryDriver = createMockDriver('memory');
156
+ const tursoDriver = createMockDriver('turso');
157
+
158
+ engine.registerDriver(memoryDriver);
159
+ engine.registerDriver(tursoDriver);
160
+
161
+ engine.setDatasourceMapping([
162
+ { namespace: 'crm', datasource: 'memory' },
163
+ ]);
164
+
165
+ // Object explicitly sets datasource
166
+ SchemaRegistry.registerObject(
167
+ {
168
+ name: 'account',
169
+ datasource: 'turso', // Explicit override
170
+ fields: { name: { type: 'text' } },
171
+ },
172
+ 'com.example.crm',
173
+ 'crm',
174
+ 'own'
175
+ );
176
+
177
+ // Should use turso (explicit) not memory (mapping)
178
+ const result = await engine.insert('account', { name: 'Test' });
179
+ expect(result).toBeDefined();
180
+ });
181
+ });
@@ -6,6 +6,7 @@ import type { IDataDriver } from '@objectstack/spec/contracts';
6
6
  // Mock the SchemaRegistry to avoid side effects between tests
7
7
  vi.mock('./registry', () => {
8
8
  const mockObjects = new Map();
9
+ const mockContributors = new Map();
9
10
  return {
10
11
  SchemaRegistry: {
11
12
  getObject: vi.fn((name) => mockObjects.get(name)),
@@ -13,8 +14,18 @@ vi.mock('./registry', () => {
13
14
  registerObject: vi.fn((obj, packageId, namespace, ownership, priority) => {
14
15
  const fqn = namespace ? `${namespace}__${obj.name}` : obj.name;
15
16
  mockObjects.set(fqn, { ...obj, name: fqn });
17
+ // Also track contributors for getObjectOwner
18
+ if (!mockContributors.has(fqn)) {
19
+ mockContributors.set(fqn, []);
20
+ }
21
+ const contributors = mockContributors.get(fqn);
22
+ contributors.push({ packageId, namespace, ownership, priority, definition: obj });
16
23
  return fqn;
17
24
  }),
25
+ getObjectOwner: vi.fn((fqn) => {
26
+ const contributors = mockContributors.get(fqn);
27
+ return contributors?.find(c => c.ownership === 'own');
28
+ }),
18
29
  registerNamespace: vi.fn(),
19
30
  registerKind: vi.fn(),
20
31
  registerItem: vi.fn(),
@@ -25,7 +36,10 @@ vi.mock('./registry', () => {
25
36
  enabled: true,
26
37
  installedAt: new Date().toISOString(),
27
38
  })),
28
- reset: vi.fn(() => mockObjects.clear()),
39
+ reset: vi.fn(() => {
40
+ mockObjects.clear();
41
+ mockContributors.clear();
42
+ }),
29
43
  metadata: {
30
44
  get: vi.fn(() => mockObjects) // Expose for verification if needed
31
45
  }
package/src/engine.ts CHANGED
@@ -1,17 +1,19 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
3
  import { QueryAST, HookContext, ServiceObject } from '@objectstack/spec/data';
4
- import {
4
+ import {
5
5
  EngineQueryOptions,
6
- DataEngineInsertOptions,
7
- EngineUpdateOptions,
6
+ DataEngineInsertOptions,
7
+ EngineUpdateOptions,
8
8
  EngineDeleteOptions,
9
9
  EngineAggregateOptions,
10
- EngineCountOptions
10
+ EngineCountOptions
11
11
  } from '@objectstack/spec/data';
12
12
  import { ExecutionContext, ExecutionContextSchema } from '@objectstack/spec/kernel';
13
13
  import { DriverInterface, IDataEngine, Logger, createLogger } from '@objectstack/core';
14
14
  import { CoreServiceName } from '@objectstack/spec/system';
15
+ import { IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';
16
+ import { pluralToSingular } from '@objectstack/spec/shared';
15
17
  import { SchemaRegistry } from './registry.js';
16
18
 
17
19
  export type HookHandler = (context: HookContext) => Promise<void> | void;
@@ -69,7 +71,20 @@ export class ObjectQL implements IDataEngine {
69
71
  private drivers = new Map<string, DriverInterface>();
70
72
  private defaultDriver: string | null = null;
71
73
  private logger: Logger;
72
-
74
+
75
+ // Datasource mapping rules (imported from defineStack)
76
+ private datasourceMapping: Array<{
77
+ namespace?: string;
78
+ package?: string;
79
+ objectPattern?: string;
80
+ default?: boolean;
81
+ datasource: string;
82
+ priority?: number;
83
+ }> = [];
84
+
85
+ // Package manifests registry (for defaultDatasource lookup)
86
+ private manifests = new Map<string, any>();
87
+
73
88
  // Per-object hooks with priority support
74
89
  private hooks: Map<string, HookEntry[]> = new Map([
75
90
  ['beforeFind', []], ['afterFind', []],
@@ -86,10 +101,13 @@ export class ObjectQL implements IDataEngine {
86
101
 
87
102
  // Action registry: key = "objectName:actionName"
88
103
  private actions = new Map<string, { handler: (ctx: any) => Promise<any> | any; package?: string }>();
89
-
104
+
90
105
  // Host provided context additions (e.g. Server router)
91
106
  private hostContext: Record<string, any> = {};
92
107
 
108
+ // Realtime service for event publishing
109
+ private realtimeService?: IRealtimeService;
110
+
93
111
  constructor(hostContext: Record<string, any> = {}) {
94
112
  this.hostContext = hostContext;
95
113
  // Use provided logger or create a new one
@@ -303,6 +321,11 @@ export class ObjectQL implements IDataEngine {
303
321
  const namespace = manifest.namespace as string | undefined;
304
322
  this.logger.debug('Registering package manifest', { id, namespace });
305
323
 
324
+ // Store manifest for defaultDatasource lookup
325
+ if (id) {
326
+ this.manifests.set(id, manifest);
327
+ }
328
+
306
329
  // 1. Register the Package (manifest + lifecycle state)
307
330
  SchemaRegistry.installPackage(manifest);
308
331
  this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name, namespace });
@@ -391,7 +414,7 @@ export class ObjectQL implements IDataEngine {
391
414
  for (const item of items) {
392
415
  const itemName = item.name || item.id;
393
416
  if (itemName) {
394
- SchemaRegistry.registerItem(key, item, 'name' as any, id);
417
+ SchemaRegistry.registerItem(pluralToSingular(key), item, 'name' as any, id);
395
418
  }
396
419
  }
397
420
  }
@@ -497,7 +520,7 @@ export class ObjectQL implements IDataEngine {
497
520
  for (const item of items) {
498
521
  const itemName = item.name || item.id;
499
522
  if (itemName) {
500
- SchemaRegistry.registerItem(key, item, 'name' as any, ownerId);
523
+ SchemaRegistry.registerItem(pluralToSingular(key), item, 'name' as any, ownerId);
501
524
  }
502
525
  }
503
526
  }
@@ -514,8 +537,8 @@ export class ObjectQL implements IDataEngine {
514
537
  }
515
538
 
516
539
  this.drivers.set(driver.name, driver);
517
- this.logger.info('Registered driver', {
518
- driverName: driver.name,
540
+ this.logger.info('Registered driver', {
541
+ driverName: driver.name,
519
542
  version: driver.version
520
543
  });
521
544
 
@@ -525,6 +548,17 @@ export class ObjectQL implements IDataEngine {
525
548
  }
526
549
  }
527
550
 
551
+ /**
552
+ * Set the realtime service for publishing data change events.
553
+ * Should be called after kernel resolves the realtime service.
554
+ *
555
+ * @param service - An IRealtimeService instance for event publishing
556
+ */
557
+ setRealtimeService(service: IRealtimeService): void {
558
+ this.realtimeService = service;
559
+ this.logger.info('RealtimeService configured for data events');
560
+ }
561
+
528
562
  /**
529
563
  * Helper to get object definition
530
564
  */
@@ -555,36 +589,138 @@ export class ObjectQL implements IDataEngine {
555
589
 
556
590
  /**
557
591
  * Helper to get the target driver
592
+ *
593
+ * Resolution priority (first match wins):
594
+ * 1. Object's explicit `datasource` field (if not 'default')
595
+ * 2. DatasourceMapping rules (namespace/package/pattern matching)
596
+ * 3. Package's `defaultDatasource` from manifest
597
+ * 4. Global default driver
558
598
  */
559
599
  private getDriver(objectName: string): DriverInterface {
560
600
  const object = SchemaRegistry.getObject(objectName);
561
-
562
- // 1. If object definition exists, check for explicit datasource
563
- if (object) {
564
- const datasourceName = object.datasource || 'default';
565
-
566
- // If configured for 'default', try to find the default driver
567
- if (datasourceName === 'default') {
568
- if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
569
- return this.drivers.get(this.defaultDriver)!;
570
- }
571
- } else {
572
- // Specific datasource requested
573
- if (this.drivers.has(datasourceName)) {
574
- return this.drivers.get(datasourceName)!;
601
+
602
+ // 1. Object's explicit datasource field (highest priority)
603
+ if (object?.datasource && object.datasource !== 'default') {
604
+ if (this.drivers.has(object.datasource)) {
605
+ return this.drivers.get(object.datasource)!;
606
+ }
607
+ throw new Error(`[ObjectQL] Datasource '${object.datasource}' configured for object '${objectName}' is not registered.`);
608
+ }
609
+
610
+ // 2. Check datasourceMapping rules
611
+ const mappedDatasource = this.resolveDatasourceFromMapping(objectName, object);
612
+ if (mappedDatasource && this.drivers.has(mappedDatasource)) {
613
+ this.logger.debug('Resolved datasource from mapping', {
614
+ object: objectName,
615
+ datasource: mappedDatasource
616
+ });
617
+ return this.drivers.get(mappedDatasource)!;
618
+ }
619
+
620
+ // 3. Check package's defaultDatasource
621
+ // Use the object's FQN name (from getObject) for ownership lookup
622
+ const fqn = object?.name || objectName;
623
+ const owner = SchemaRegistry.getObjectOwner(fqn);
624
+ if (owner?.packageId) {
625
+ const manifest = this.manifests.get(owner.packageId);
626
+ if (manifest?.defaultDatasource && manifest.defaultDatasource !== 'default') {
627
+ if (this.drivers.has(manifest.defaultDatasource)) {
628
+ this.logger.debug('Resolved datasource from package manifest', {
629
+ object: objectName,
630
+ package: owner.packageId,
631
+ datasource: manifest.defaultDatasource
632
+ });
633
+ return this.drivers.get(manifest.defaultDatasource)!;
575
634
  }
576
- throw new Error(`[ObjectQL] Datasource '${datasourceName}' configured for object '${objectName}' is not registered.`);
577
635
  }
578
636
  }
579
637
 
580
- // 2. Fallback for ad-hoc objects or missing definitions
581
- if (this.defaultDriver) {
638
+ // 4. Fallback to global default driver
639
+ if (this.defaultDriver && this.drivers.has(this.defaultDriver)) {
582
640
  return this.drivers.get(this.defaultDriver)!;
583
641
  }
584
642
 
585
643
  throw new Error(`[ObjectQL] No driver available for object '${objectName}'`);
586
644
  }
587
645
 
646
+ /**
647
+ * Resolve datasource from mapping rules
648
+ *
649
+ * Rules are evaluated in order (or by priority if specified).
650
+ * First matching rule wins.
651
+ */
652
+ private resolveDatasourceFromMapping(
653
+ objectName: string,
654
+ object?: any
655
+ ): string | null {
656
+ if (!this.datasourceMapping || this.datasourceMapping.length === 0) {
657
+ return null;
658
+ }
659
+
660
+ // Sort rules by priority if any have priority set
661
+ const sortedRules = [...this.datasourceMapping].sort((a, b) => {
662
+ const aPriority = a.priority ?? 1000;
663
+ const bPriority = b.priority ?? 1000;
664
+ return aPriority - bPriority;
665
+ });
666
+
667
+ for (const rule of sortedRules) {
668
+ // 1. Match by namespace
669
+ if (rule.namespace && object?.namespace === rule.namespace) {
670
+ return rule.datasource;
671
+ }
672
+
673
+ // 2. Match by package ID
674
+ if (rule.package && object?.packageId === rule.package) {
675
+ return rule.datasource;
676
+ }
677
+
678
+ // 3. Match by object name pattern (glob-style)
679
+ if (rule.objectPattern && this.matchPattern(objectName, rule.objectPattern)) {
680
+ return rule.datasource;
681
+ }
682
+
683
+ // 4. Default fallback rule
684
+ if (rule.default) {
685
+ return rule.datasource;
686
+ }
687
+ }
688
+
689
+ return null;
690
+ }
691
+
692
+ /**
693
+ * Simple glob pattern matching
694
+ * Supports * (any chars) and ? (single char)
695
+ */
696
+ private matchPattern(objectName: string, pattern: string): boolean {
697
+ const regexPattern = pattern
698
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
699
+ .replace(/\*/g, '.*') // * → .*
700
+ .replace(/\?/g, '.'); // ? → .
701
+
702
+ const regex = new RegExp(`^${regexPattern}$`);
703
+ return regex.test(objectName);
704
+ }
705
+
706
+ /**
707
+ * Set datasource mapping rules
708
+ * Called by ObjectQLPlugin during bootstrap
709
+ */
710
+ setDatasourceMapping(rules: Array<{
711
+ namespace?: string;
712
+ package?: string;
713
+ objectPattern?: string;
714
+ default?: boolean;
715
+ datasource: string;
716
+ priority?: number;
717
+ }>) {
718
+ this.datasourceMapping = rules;
719
+ this.logger.info('Datasource mapping rules configured', {
720
+ ruleCount: rules.length
721
+ });
722
+ }
723
+
588
724
  /**
589
725
  * Initialize the engine and all registered drivers
590
726
  */
@@ -594,14 +730,24 @@ export class ObjectQL implements IDataEngine {
594
730
  drivers: Array.from(this.drivers.keys())
595
731
  });
596
732
 
733
+ const failedDrivers: string[] = [];
597
734
  for (const [name, driver] of this.drivers) {
598
735
  try {
599
736
  await driver.connect();
600
737
  this.logger.info('Driver connected successfully', { driverName: name });
601
738
  } catch (e) {
739
+ failedDrivers.push(name);
602
740
  this.logger.error('Failed to connect driver', e as Error, { driverName: name });
603
741
  }
604
742
  }
743
+
744
+ if (failedDrivers.length > 0) {
745
+ this.logger.warn(
746
+ `${failedDrivers.length} of ${this.drivers.size} driver(s) failed initial connect. ` +
747
+ `Operations may recover via lazy reconnection or fail at query time.`,
748
+ { failedDrivers }
749
+ );
750
+ }
605
751
 
606
752
  this.logger.info('ObjectQL engine initialization complete');
607
753
  }
@@ -873,6 +1019,42 @@ export class ObjectQL implements IDataEngine {
873
1019
  hookContext.result = result;
874
1020
  await this.triggerHooks('afterInsert', hookContext);
875
1021
 
1022
+ // Publish data.record.created event to realtime service
1023
+ if (this.realtimeService) {
1024
+ try {
1025
+ if (Array.isArray(result)) {
1026
+ // Bulk insert - publish event for each record
1027
+ for (const record of result) {
1028
+ const event: RealtimeEventPayload = {
1029
+ type: 'data.record.created',
1030
+ object,
1031
+ payload: {
1032
+ recordId: record.id,
1033
+ after: record,
1034
+ },
1035
+ timestamp: new Date().toISOString(),
1036
+ };
1037
+ await this.realtimeService.publish(event);
1038
+ }
1039
+ this.logger.debug(`Published ${result.length} data.record.created events`, { object });
1040
+ } else {
1041
+ const event: RealtimeEventPayload = {
1042
+ type: 'data.record.created',
1043
+ object,
1044
+ payload: {
1045
+ recordId: result.id,
1046
+ after: result,
1047
+ },
1048
+ timestamp: new Date().toISOString(),
1049
+ };
1050
+ await this.realtimeService.publish(event);
1051
+ this.logger.debug('Published data.record.created event', { object, recordId: result.id });
1052
+ }
1053
+ } catch (error) {
1054
+ this.logger.warn('Failed to publish data event', { object, error });
1055
+ }
1056
+ }
1057
+
876
1058
  return hookContext.result;
877
1059
  } catch (e) {
878
1060
  this.logger.error('Insert operation failed', e as Error, { object });
@@ -927,6 +1109,29 @@ export class ObjectQL implements IDataEngine {
927
1109
  hookContext.event = 'afterUpdate';
928
1110
  hookContext.result = result;
929
1111
  await this.triggerHooks('afterUpdate', hookContext);
1112
+
1113
+ // Publish data.record.updated event to realtime service
1114
+ if (this.realtimeService) {
1115
+ try {
1116
+ const resultId = (typeof result === 'object' && result && 'id' in result) ? (result as any).id : undefined;
1117
+ const recordId = String(hookContext.input.id || resultId || '');
1118
+ const event: RealtimeEventPayload = {
1119
+ type: 'data.record.updated',
1120
+ object,
1121
+ payload: {
1122
+ recordId,
1123
+ changes: hookContext.input.data,
1124
+ after: result,
1125
+ },
1126
+ timestamp: new Date().toISOString(),
1127
+ };
1128
+ await this.realtimeService.publish(event);
1129
+ this.logger.debug('Published data.record.updated event', { object, recordId });
1130
+ } catch (error) {
1131
+ this.logger.warn('Failed to publish data event', { object, error });
1132
+ }
1133
+ }
1134
+
930
1135
  return hookContext.result;
931
1136
  } catch (e) {
932
1137
  this.logger.error('Update operation failed', e as Error, { object });
@@ -980,6 +1185,27 @@ export class ObjectQL implements IDataEngine {
980
1185
  hookContext.event = 'afterDelete';
981
1186
  hookContext.result = result;
982
1187
  await this.triggerHooks('afterDelete', hookContext);
1188
+
1189
+ // Publish data.record.deleted event to realtime service
1190
+ if (this.realtimeService) {
1191
+ try {
1192
+ const resultId = (typeof result === 'object' && result && 'id' in result) ? (result as any).id : undefined;
1193
+ const recordId = String(hookContext.input.id || resultId || '');
1194
+ const event: RealtimeEventPayload = {
1195
+ type: 'data.record.deleted',
1196
+ object,
1197
+ payload: {
1198
+ recordId,
1199
+ },
1200
+ timestamp: new Date().toISOString(),
1201
+ };
1202
+ await this.realtimeService.publish(event);
1203
+ this.logger.debug('Published data.record.deleted event', { object, recordId });
1204
+ } catch (error) {
1205
+ this.logger.warn('Failed to publish data event', { object, error });
1206
+ }
1207
+ }
1208
+
983
1209
  return hookContext.result;
984
1210
  } catch (e) {
985
1211
  this.logger.error('Delete operation failed', e as Error, { object });