@objectstack/runtime 3.2.1 → 3.2.3

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/runtime",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
4
4
  "license": "Apache-2.0",
5
5
  "description": "ObjectStack Core Runtime & Query Engine",
6
6
  "type": "module",
@@ -15,10 +15,10 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "zod": "^4.3.6",
18
- "@objectstack/core": "3.2.1",
19
- "@objectstack/spec": "3.2.1",
20
- "@objectstack/types": "3.2.1",
21
- "@objectstack/rest": "3.2.1"
18
+ "@objectstack/core": "3.2.3",
19
+ "@objectstack/rest": "3.2.3",
20
+ "@objectstack/spec": "3.2.3",
21
+ "@objectstack/types": "3.2.3"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "^5.0.0",
package/src/app-plugin.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
3
  import { Plugin, PluginContext } from '@objectstack/core';
4
+ import { SeedLoaderService } from './seed-loader.js';
5
+ import type { IMetadataService } from '@objectstack/spec/contracts';
4
6
 
5
7
  /**
6
8
  * AppPlugin
@@ -127,24 +129,60 @@ export class AppPlugin implements Plugin {
127
129
 
128
130
  if (seedDatasets.length > 0) {
129
131
  ctx.logger.info(`[AppPlugin] Found ${seedDatasets.length} seed datasets for ${appId}`);
130
- for (const dataset of seedDatasets) {
131
- if (dataset.object && Array.isArray(dataset.records)) {
132
- const objectFQN = toFQN(dataset.object);
133
- ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${objectFQN}`);
132
+
133
+ // Normalize dataset object names to FQN
134
+ const normalizedDatasets = seedDatasets
135
+ .filter((d: any) => d.object && Array.isArray(d.records))
136
+ .map((d: any) => ({
137
+ ...d,
138
+ object: toFQN(d.object),
139
+ }));
140
+
141
+ // Use SeedLoaderService for metadata-driven loading with reference resolution
142
+ try {
143
+ const metadata = ctx.getService('metadata') as IMetadataService | undefined;
144
+ if (metadata) {
145
+ const seedLoader = new SeedLoaderService(ql, metadata, ctx.logger);
146
+ const { SeedLoaderRequestSchema } = await import('@objectstack/spec/data');
147
+ const request = SeedLoaderRequestSchema.parse({
148
+ datasets: normalizedDatasets,
149
+ config: { defaultMode: 'upsert', multiPass: true },
150
+ });
151
+ const result = await seedLoader.load(request);
152
+ ctx.logger.info('[Seeder] Seed loading complete', {
153
+ inserted: result.summary.totalInserted,
154
+ updated: result.summary.totalUpdated,
155
+ errors: result.errors.length,
156
+ });
157
+ } else {
158
+ // Fallback: basic insert when metadata service is not available
159
+ ctx.logger.debug('[Seeder] No metadata service; using basic insert fallback');
160
+ for (const dataset of normalizedDatasets) {
161
+ ctx.logger.info(`[Seeder] Seeding ${dataset.records.length} records for ${dataset.object}`);
162
+ for (const record of dataset.records) {
163
+ try {
164
+ await ql.insert(dataset.object, record);
165
+ } catch (err: any) {
166
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: err.message });
167
+ }
168
+ }
169
+ }
170
+ ctx.logger.info('[Seeder] Data seeding complete.');
171
+ }
172
+ } catch (err: any) {
173
+ // If SeedLoaderService fails (e.g., metadata not available), fall back to basic insert
174
+ ctx.logger.warn('[Seeder] SeedLoaderService failed, falling back to basic insert', { error: err.message });
175
+ for (const dataset of normalizedDatasets) {
134
176
  for (const record of dataset.records) {
135
- try {
136
- // Use ObjectQL engine to insert data
137
- // This ensures driver resolution and hook execution
138
- // Use 'insert' which corresponds to 'create' in driver
139
- await ql.insert(objectFQN, record);
140
- } catch (err: any) {
141
- // Ignore duplicate errors if needed, or log/warn
142
- ctx.logger.warn(`[Seeder] Failed to insert ${objectFQN} record:`, { error: err.message });
143
- }
177
+ try {
178
+ await ql.insert(dataset.object, record);
179
+ } catch (insertErr: any) {
180
+ ctx.logger.warn(`[Seeder] Failed to insert ${dataset.object} record:`, { error: insertErr.message });
181
+ }
144
182
  }
145
183
  }
184
+ ctx.logger.info('[Seeder] Data seeding complete (fallback).');
146
185
  }
147
- ctx.logger.info('[Seeder] Data seeding complete.');
148
186
  }
149
187
  }
150
188
  }
@@ -216,7 +216,7 @@ describe('HttpDispatcher', () => {
216
216
  it('should resolve analytics service from Promise (async factory)', async () => {
217
217
  const mockAnalytics = {
218
218
  query: vi.fn().mockResolvedValue({ rows: [{ id: 1 }], total: 1 }),
219
- getMetadata: vi.fn().mockResolvedValue({ tables: ['t1'] }),
219
+ getMeta: vi.fn().mockResolvedValue({ tables: ['t1'] }),
220
220
  generateSql: vi.fn().mockResolvedValue({ sql: 'SELECT 1' }),
221
221
  };
222
222
  // Inject as Promise (simulates async factory registration)
@@ -245,7 +245,7 @@ describe('HttpDispatcher', () => {
245
245
 
246
246
  it('should handle GET /analytics/meta with async service', async () => {
247
247
  const mockAnalytics = {
248
- getMetadata: vi.fn().mockResolvedValue({ tables: ['users', 'orders'] }),
248
+ getMeta: vi.fn().mockResolvedValue({ tables: ['users', 'orders'] }),
249
249
  };
250
250
  (kernel as any).getService = vi.fn().mockResolvedValue(mockAnalytics);
251
251
 
@@ -600,7 +600,7 @@ describe('HttpDispatcher', () => {
600
600
 
601
601
  describe('handleData', () => {
602
602
  it('should pass expand and select to broker for GET /data/:object/:id', async () => {
603
- mockBroker.call.mockResolvedValue({ object: 'order_item', id: 'oi_1', record: { _id: 'oi_1' } });
603
+ mockBroker.call.mockResolvedValue({ object: 'order_item', id: 'oi_1', record: { id: 'oi_1' } });
604
604
 
605
605
  const result = await dispatcher.handleData(
606
606
  '/order_item/oi_1', 'GET', {},
@@ -526,7 +526,7 @@ export class HttpDispatcher {
526
526
  * Handles Analytics requests
527
527
  * path: sub-path after /analytics/
528
528
  */
529
- async handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> {
529
+ async handleAnalytics(path: string, method: string, body: any, _context: HttpProtocolContext): Promise<HttpDispatcherResult> {
530
530
  const analyticsService = await this.getService(CoreServiceName.enum.analytics);
531
531
  if (!analyticsService) return { handled: false }; // 404 handled by caller if unhandled
532
532
 
@@ -535,20 +535,20 @@ export class HttpDispatcher {
535
535
 
536
536
  // POST /analytics/query
537
537
  if (subPath === 'query' && m === 'POST') {
538
- const result = await analyticsService.query(body, { request: context.request });
538
+ const result = await analyticsService.query(body);
539
539
  return { handled: true, response: this.success(result) };
540
540
  }
541
541
 
542
542
  // GET /analytics/meta
543
543
  if (subPath === 'meta' && m === 'GET') {
544
- const result = await analyticsService.getMetadata({ request: context.request });
544
+ const result = await analyticsService.getMeta();
545
545
  return { handled: true, response: this.success(result) };
546
546
  }
547
547
 
548
548
  // POST /analytics/sql (Dry-run or debug)
549
549
  if (subPath === 'sql' && m === 'POST') {
550
550
  // Assuming service has generateSql method
551
- const result = await analyticsService.generateSql(body, { request: context.request });
551
+ const result = await analyticsService.generateSql(body);
552
552
  return { handled: true, response: this.success(result) };
553
553
  }
554
554
 
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export type { RuntimeConfig } from './runtime.js';
10
10
  // Export Plugins
11
11
  export { DriverPlugin } from './driver-plugin.js';
12
12
  export { AppPlugin } from './app-plugin.js';
13
+ export { SeedLoaderService } from './seed-loader.js';
13
14
  export { createDispatcherPlugin } from './dispatcher-plugin.js';
14
15
  export type { DispatcherPluginConfig } from './dispatcher-plugin.js';
15
16