@objectstack/objectql 1.1.0 → 2.0.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/src/engine.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
1
3
  import { QueryAST, HookContext } from '@objectstack/spec/data';
2
4
  import {
3
5
  DataEngineQueryOptions,
@@ -219,8 +221,20 @@ export class ObjectQL implements IDataEngine {
219
221
 
220
222
  // 5. Register all other metadata types generically
221
223
  const metadataArrayKeys = [
222
- 'actions', 'dashboards', 'reports', 'flows', 'agents',
223
- 'apis', 'ragPipelines', 'profiles', 'sharingRules'
224
+ // UI Protocol
225
+ 'actions', 'views', 'pages', 'dashboards', 'reports', 'themes',
226
+ // Automation Protocol
227
+ 'flows', 'workflows', 'approvals', 'webhooks',
228
+ // Security Protocol
229
+ 'roles', 'permissions', 'profiles', 'sharingRules', 'policies',
230
+ // AI Protocol
231
+ 'agents', 'ragPipelines',
232
+ // API Protocol
233
+ 'apis',
234
+ // Data Extensions
235
+ 'hooks', 'mappings', 'analyticsCubes',
236
+ // Integration Protocol
237
+ 'connectors',
224
238
  ];
225
239
  for (const key of metadataArrayKeys) {
226
240
  const items = (manifest as any)[key];
@@ -425,13 +439,13 @@ export class ObjectQL implements IDataEngine {
425
439
  await this.triggerHooks('beforeFind', hookContext);
426
440
 
427
441
  try {
428
- const result = await driver.find(object, hookContext.input.ast, hookContext.input.options);
442
+ const result = await driver.find(object, hookContext.input.ast as QueryAST, hookContext.input.options as any);
429
443
 
430
444
  hookContext.event = 'afterFind';
431
445
  hookContext.result = result;
432
446
  await this.triggerHooks('afterFind', hookContext);
433
447
 
434
- return hookContext.result;
448
+ return hookContext.result as any[];
435
449
  } catch (e) {
436
450
  this.logger.error('Find operation failed', e as Error, { object });
437
451
  throw e;
@@ -468,13 +482,13 @@ export class ObjectQL implements IDataEngine {
468
482
  if (Array.isArray(hookContext.input.data)) {
469
483
  // Bulk Create
470
484
  if (driver.bulkCreate) {
471
- result = await driver.bulkCreate(object, hookContext.input.data, hookContext.input.options);
485
+ result = await driver.bulkCreate(object, hookContext.input.data as any[], hookContext.input.options as any);
472
486
  } else {
473
487
  // Fallback loop
474
- result = await Promise.all(hookContext.input.data.map((item: any) => driver.create(object, item, hookContext.input.options)));
488
+ result = await Promise.all((hookContext.input.data as any[]).map((item: any) => driver.create(object, item, hookContext.input.options as any)));
475
489
  }
476
490
  } else {
477
- result = await driver.create(object, hookContext.input.data, hookContext.input.options);
491
+ result = await driver.create(object, hookContext.input.data, hookContext.input.options as any);
478
492
  }
479
493
 
480
494
  hookContext.event = 'afterInsert';
@@ -517,11 +531,11 @@ export class ObjectQL implements IDataEngine {
517
531
  let result;
518
532
  if (hookContext.input.id) {
519
533
  // Single update by ID
520
- result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
534
+ result = await driver.update(object, hookContext.input.id as string, hookContext.input.data, hookContext.input.options as any);
521
535
  } else if (options?.multi && driver.updateMany) {
522
536
  // Bulk update by Query
523
537
  const ast = this.toQueryAST(object, { filter: options.filter });
524
- result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options);
538
+ result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options as any);
525
539
  } else {
526
540
  throw new Error('Update requires an ID or options.multi=true');
527
541
  }
@@ -560,10 +574,10 @@ export class ObjectQL implements IDataEngine {
560
574
  try {
561
575
  let result;
562
576
  if (hookContext.input.id) {
563
- result = await driver.delete(object, hookContext.input.id, hookContext.input.options);
577
+ result = await driver.delete(object, hookContext.input.id as string, hookContext.input.options as any);
564
578
  } else if (options?.multi && driver.deleteMany) {
565
579
  const ast = this.toQueryAST(object, { filter: options.filter });
566
- result = await driver.deleteMany(object, ast, hookContext.input.options);
580
+ result = await driver.deleteMany(object, ast, hookContext.input.options as any);
567
581
  } else {
568
582
  throw new Error('Delete requires an ID or options.multi=true');
569
583
  }
@@ -594,10 +608,22 @@ export class ObjectQL implements IDataEngine {
594
608
  object = this.resolveObjectName(object);
595
609
  const driver = this.getDriver(object);
596
610
  this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
597
- // Driver needs support for raw aggregation or mapped aggregation
598
- // For now, if driver supports 'execute', we might pass it down, or we need to add 'aggregate' to DriverInterface
599
- // In this version, we'll assume driver might handle it via special 'find' or throw not implemented
600
- throw new Error('Aggregate not yet fully implemented in ObjectQL->Driver mapping');
611
+
612
+ // Build a QueryAST with groupBy and aggregations, delegate to driver.find()
613
+ // Drivers that support aggregation (e.g. InMemoryDriver) handle groupBy/aggregations
614
+ // in their find() implementation via performAggregation().
615
+ const ast: QueryAST = {
616
+ object,
617
+ where: query.filter,
618
+ groupBy: query.groupBy,
619
+ aggregations: query.aggregations?.map(agg => ({
620
+ function: agg.method,
621
+ field: agg.field,
622
+ alias: agg.alias || `${agg.method}_${agg.field || 'all'}`,
623
+ })),
624
+ };
625
+
626
+ return driver.find(object, ast);
601
627
  }
602
628
 
603
629
  async execute(command: any, options?: Record<string, any>): Promise<any> {
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
1
3
  // Export Registry
2
4
  export {
3
5
  SchemaRegistry,
package/src/plugin.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
1
3
  import { ObjectQL } from './engine.js';
2
4
  import { ObjectStackProtocolImplementation } from './protocol.js';
3
5
  import { Plugin, PluginContext } from '@objectstack/core';
package/src/protocol.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
1
3
  import { ObjectStackProtocol } from '@objectstack/spec/api';
2
4
  import { IDataEngine } from '@objectstack/core';
3
5
  import type {
@@ -32,7 +34,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
32
34
  this.engine = engine;
33
35
  }
34
36
 
35
- async getDiscovery(_request: {}) {
37
+ async getDiscovery() {
36
38
  return {
37
39
  version: '1.0',
38
40
  apiName: 'ObjectStack API',
@@ -40,36 +42,74 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
40
42
  graphql: false,
41
43
  search: false,
42
44
  websockets: false,
43
- files: true,
44
- analytics: false,
45
- hub: false
45
+ files: false,
46
+ analytics: true,
47
+ ai: false,
48
+ workflow: false,
49
+ notifications: false,
50
+ i18n: false,
46
51
  },
47
52
  endpoints: {
48
53
  data: '/api/data',
49
54
  metadata: '/api/meta',
50
- auth: '/api/auth'
51
- }
55
+ analytics: '/api/analytics',
56
+ },
57
+ services: {
58
+ // --- Kernel-provided (objectql is an example kernel implementation) ---
59
+ metadata: { enabled: true, status: 'degraded' as const, route: '/api/meta', provider: 'objectql', message: 'In-memory registry only; DB persistence not yet implemented' },
60
+ data: { enabled: true, status: 'available' as const, route: '/api/data', provider: 'objectql' },
61
+ analytics: { enabled: true, status: 'available' as const, route: '/api/analytics', provider: 'objectql' },
62
+ // --- Plugin-provided (kernel does NOT handle these) ---
63
+ auth: { enabled: false, status: 'unavailable' as const, message: 'Install an auth plugin (e.g. plugin-auth) to enable' },
64
+ automation: { enabled: false, status: 'unavailable' as const, message: 'Install an automation plugin (e.g. plugin-automation) to enable' },
65
+ // --- Core infrastructure (plugin-provided) ---
66
+ cache: { enabled: false, status: 'unavailable' as const, message: 'Install a cache plugin (e.g. plugin-redis) to enable' },
67
+ queue: { enabled: false, status: 'unavailable' as const, message: 'Install a queue plugin (e.g. plugin-bullmq) to enable' },
68
+ job: { enabled: false, status: 'unavailable' as const, message: 'Install a job scheduler plugin to enable' },
69
+ // --- Optional services (all plugin-provided) ---
70
+ ui: { enabled: false, status: 'unavailable' as const, message: 'Install a UI plugin to enable' },
71
+ workflow: { enabled: false, status: 'unavailable' as const, message: 'Install a workflow plugin to enable' },
72
+ realtime: { enabled: false, status: 'unavailable' as const, message: 'Install a realtime plugin to enable' },
73
+ notification: { enabled: false, status: 'unavailable' as const, message: 'Install a notification plugin to enable' },
74
+ ai: { enabled: false, status: 'unavailable' as const, message: 'Install an AI plugin to enable' },
75
+ i18n: { enabled: false, status: 'unavailable' as const, message: 'Install an i18n plugin to enable' },
76
+ graphql: { enabled: false, status: 'unavailable' as const, message: 'Install a GraphQL plugin to enable' },
77
+ 'file-storage': { enabled: false, status: 'unavailable' as const, message: 'Install a file-storage plugin to enable' },
78
+ search: { enabled: false, status: 'unavailable' as const, message: 'Install a search plugin to enable' },
79
+ },
52
80
  };
53
81
  }
54
82
 
55
- async getMetaTypes(_request: {}) {
83
+ async getMetaTypes() {
56
84
  return {
57
85
  types: SchemaRegistry.getRegisteredTypes()
58
86
  };
59
87
  }
60
88
 
61
- async getMetaItems(request: { type: string; packageId?: string }) {
89
+ async getMetaItems(request: { type: string }) {
90
+ let items = SchemaRegistry.listItems(request.type);
91
+ // Normalize singular/plural: REST uses singular ('app') but registry may store as plural ('apps')
92
+ if (items.length === 0) {
93
+ const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
94
+ items = SchemaRegistry.listItems(alt);
95
+ }
62
96
  return {
63
97
  type: request.type,
64
- items: SchemaRegistry.listItems(request.type, request.packageId)
98
+ items
65
99
  };
66
100
  }
67
101
 
68
102
  async getMetaItem(request: { type: string, name: string }) {
103
+ let item = SchemaRegistry.getItem(request.type, request.name);
104
+ // Normalize singular/plural
105
+ if (item === undefined) {
106
+ const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
107
+ item = SchemaRegistry.getItem(alt, request.name);
108
+ }
69
109
  return {
70
110
  type: request.type,
71
111
  name: request.name,
72
- item: SchemaRegistry.getItem(request.type, request.name)
112
+ item
73
113
  };
74
114
  }
75
115
 
@@ -258,12 +298,84 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
258
298
  // Batch Operations
259
299
  // ==========================================
260
300
 
261
- async batchData(_request: { object: string, request: BatchUpdateRequest }): Promise<BatchUpdateResponse> {
262
- // Map high-level batch request to DataEngine batch if available
263
- // Or implement loop here.
264
- // For now, let's just fail or implement basic loop to satisfying interface
265
- // since full batch mapping requires careful type handling.
266
- throw new Error('Batch operations not yet fully implemented in protocol adapter');
301
+ async batchData(request: { object: string, request: BatchUpdateRequest }): Promise<BatchUpdateResponse> {
302
+ const { object, request: batchReq } = request;
303
+ const { operation, records, options } = batchReq;
304
+ const results: Array<{ id?: string; success: boolean; error?: string; record?: any }> = [];
305
+ let succeeded = 0;
306
+ let failed = 0;
307
+
308
+ for (const record of records) {
309
+ try {
310
+ switch (operation) {
311
+ case 'create': {
312
+ const created = await this.engine.insert(object, record.data || record);
313
+ results.push({ id: created._id || created.id, success: true, record: created });
314
+ succeeded++;
315
+ break;
316
+ }
317
+ case 'update': {
318
+ if (!record.id) throw new Error('Record id is required for update');
319
+ const updated = await this.engine.update(object, record.data || {}, { filter: { _id: record.id } });
320
+ results.push({ id: record.id, success: true, record: updated });
321
+ succeeded++;
322
+ break;
323
+ }
324
+ case 'upsert': {
325
+ // Try update first, then create if not found
326
+ if (record.id) {
327
+ try {
328
+ const existing = await this.engine.findOne(object, { filter: { _id: record.id } });
329
+ if (existing) {
330
+ const updated = await this.engine.update(object, record.data || {}, { filter: { _id: record.id } });
331
+ results.push({ id: record.id, success: true, record: updated });
332
+ } else {
333
+ const created = await this.engine.insert(object, { _id: record.id, ...(record.data || {}) });
334
+ results.push({ id: created._id || created.id, success: true, record: created });
335
+ }
336
+ } catch {
337
+ const created = await this.engine.insert(object, { _id: record.id, ...(record.data || {}) });
338
+ results.push({ id: created._id || created.id, success: true, record: created });
339
+ }
340
+ } else {
341
+ const created = await this.engine.insert(object, record.data || record);
342
+ results.push({ id: created._id || created.id, success: true, record: created });
343
+ }
344
+ succeeded++;
345
+ break;
346
+ }
347
+ case 'delete': {
348
+ if (!record.id) throw new Error('Record id is required for delete');
349
+ await this.engine.delete(object, { filter: { _id: record.id } });
350
+ results.push({ id: record.id, success: true });
351
+ succeeded++;
352
+ break;
353
+ }
354
+ default:
355
+ results.push({ id: record.id, success: false, error: `Unknown operation: ${operation}` });
356
+ failed++;
357
+ }
358
+ } catch (err: any) {
359
+ results.push({ id: record.id, success: false, error: err.message });
360
+ failed++;
361
+ if (options?.atomic) {
362
+ // Abort remaining operations on first failure in atomic mode
363
+ break;
364
+ }
365
+ if (!options?.continueOnError) {
366
+ break;
367
+ }
368
+ }
369
+ }
370
+
371
+ return {
372
+ success: failed === 0,
373
+ operation,
374
+ total: records.length,
375
+ succeeded,
376
+ failed,
377
+ results: options?.returnRecords !== false ? results : results.map(r => ({ id: r.id, success: r.success, error: r.error })),
378
+ } as BatchUpdateResponse;
267
379
  }
268
380
 
269
381
  async createManyData(request: { object: string, records: any[] }): Promise<any> {
@@ -275,33 +387,211 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
275
387
  };
276
388
  }
277
389
 
278
- async updateManyData(_request: UpdateManyDataRequest): Promise<any> {
279
- // TODO: Implement proper updateMany in DataEngine
280
- throw new Error('updateManyData not implemented');
281
- }
390
+ async updateManyData(request: UpdateManyDataRequest): Promise<BatchUpdateResponse> {
391
+ const { object, records, options } = request;
392
+ const results: Array<{ id?: string; success: boolean; error?: string; record?: any }> = [];
393
+ let succeeded = 0;
394
+ let failed = 0;
395
+
396
+ for (const record of records) {
397
+ try {
398
+ const updated = await this.engine.update(object, record.data, { filter: { _id: record.id } });
399
+ results.push({ id: record.id, success: true, record: updated });
400
+ succeeded++;
401
+ } catch (err: any) {
402
+ results.push({ id: record.id, success: false, error: err.message });
403
+ failed++;
404
+ if (!options?.continueOnError) {
405
+ break;
406
+ }
407
+ }
408
+ }
282
409
 
283
- async analyticsQuery(_request: any): Promise<any> {
284
- throw new Error('analyticsQuery not implemented');
410
+ return {
411
+ success: failed === 0,
412
+ operation: 'update',
413
+ total: records.length,
414
+ succeeded,
415
+ failed,
416
+ results,
417
+ } as BatchUpdateResponse;
285
418
  }
286
419
 
287
- async getAnalyticsMeta(_request: any): Promise<any> {
288
- throw new Error('getAnalyticsMeta not implemented');
289
- }
420
+ async analyticsQuery(request: any): Promise<any> {
421
+ // Map AnalyticsQuery (cube-style) to engine aggregation.
422
+ // cube name maps to object name; measures → aggregations; dimensions → groupBy.
423
+ const { query, cube } = request;
424
+ const object = cube;
425
+
426
+ // Build groupBy from dimensions
427
+ const groupBy = query.dimensions || [];
428
+
429
+ // Build aggregations from measures
430
+ // Measures can be simple field names like "count" or "field_name.sum"
431
+ // Or cube-defined measure names. We support: field.function or just function(field).
432
+ const aggregations: Array<{ field: string; method: string; alias: string }> = [];
433
+ if (query.measures) {
434
+ for (const measure of query.measures) {
435
+ // Support formats: "count", "amount.sum", "revenue.avg"
436
+ if (measure === 'count' || measure === 'count_all') {
437
+ aggregations.push({ field: '*', method: 'count', alias: 'count' });
438
+ } else if (measure.includes('.')) {
439
+ const [field, method] = measure.split('.');
440
+ aggregations.push({ field, method, alias: `${field}_${method}` });
441
+ } else {
442
+ // Treat as count of the field
443
+ aggregations.push({ field: measure, method: 'sum', alias: measure });
444
+ }
445
+ }
446
+ }
290
447
 
291
- async triggerAutomation(_request: any): Promise<any> {
292
- throw new Error('triggerAutomation not implemented');
448
+ // Build filter from analytics filters
449
+ let filter: any = undefined;
450
+ if (query.filters && query.filters.length > 0) {
451
+ const conditions: any[] = query.filters.map((f: any) => {
452
+ const op = this.mapAnalyticsOperator(f.operator);
453
+ if (f.values && f.values.length === 1) {
454
+ return { [f.member]: { [op]: f.values[0] } };
455
+ } else if (f.values && f.values.length > 1) {
456
+ return { [f.member]: { $in: f.values } };
457
+ }
458
+ return { [f.member]: { [op]: true } };
459
+ });
460
+ filter = conditions.length === 1 ? conditions[0] : { $and: conditions };
461
+ }
462
+
463
+ // Execute via engine.aggregate (which delegates to driver.find with groupBy/aggregations)
464
+ const rows = await this.engine.aggregate(object, {
465
+ filter,
466
+ groupBy: groupBy.length > 0 ? groupBy : undefined,
467
+ aggregations: aggregations.length > 0
468
+ ? aggregations.map(a => ({ field: a.field, method: a.method as any, alias: a.alias }))
469
+ : [{ field: '*', method: 'count' as any, alias: 'count' }],
470
+ });
471
+
472
+ // Build field metadata
473
+ const fields = [
474
+ ...groupBy.map((d: string) => ({ name: d, type: 'string' })),
475
+ ...aggregations.map(a => ({ name: a.alias, type: 'number' })),
476
+ ];
477
+
478
+ return {
479
+ success: true,
480
+ data: {
481
+ rows,
482
+ fields,
483
+ },
484
+ };
293
485
  }
294
486
 
295
- async listSpaces(_request: any): Promise<any> {
296
- throw new Error('listSpaces not implemented');
487
+ async getAnalyticsMeta(request: any): Promise<any> {
488
+ // Auto-generate cube metadata from registered objects in SchemaRegistry.
489
+ // Each object becomes a cube; number fields → measures; other fields → dimensions.
490
+ const objects = SchemaRegistry.listItems('object');
491
+ const cubeFilter = request?.cube;
492
+
493
+ const cubes: any[] = [];
494
+ for (const obj of objects) {
495
+ const schema = obj as any;
496
+ if (cubeFilter && schema.name !== cubeFilter) continue;
497
+
498
+ const measures: Record<string, any> = {};
499
+ const dimensions: Record<string, any> = {};
500
+ const fields = schema.fields || {};
501
+
502
+ // Always add a count measure
503
+ measures['count'] = {
504
+ name: 'count',
505
+ label: 'Count',
506
+ type: 'count',
507
+ sql: '*',
508
+ };
509
+
510
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
511
+ const fd = fieldDef as any;
512
+ const fieldType = fd.type || 'text';
513
+
514
+ if (['number', 'currency', 'percent'].includes(fieldType)) {
515
+ // Numeric fields become both measures and dimensions
516
+ measures[`${fieldName}_sum`] = {
517
+ name: `${fieldName}_sum`,
518
+ label: `${fd.label || fieldName} (Sum)`,
519
+ type: 'sum',
520
+ sql: fieldName,
521
+ };
522
+ measures[`${fieldName}_avg`] = {
523
+ name: `${fieldName}_avg`,
524
+ label: `${fd.label || fieldName} (Avg)`,
525
+ type: 'avg',
526
+ sql: fieldName,
527
+ };
528
+ dimensions[fieldName] = {
529
+ name: fieldName,
530
+ label: fd.label || fieldName,
531
+ type: 'number',
532
+ sql: fieldName,
533
+ };
534
+ } else if (['date', 'datetime'].includes(fieldType)) {
535
+ dimensions[fieldName] = {
536
+ name: fieldName,
537
+ label: fd.label || fieldName,
538
+ type: 'time',
539
+ sql: fieldName,
540
+ granularities: ['day', 'week', 'month', 'quarter', 'year'],
541
+ };
542
+ } else if (['boolean'].includes(fieldType)) {
543
+ dimensions[fieldName] = {
544
+ name: fieldName,
545
+ label: fd.label || fieldName,
546
+ type: 'boolean',
547
+ sql: fieldName,
548
+ };
549
+ } else {
550
+ // text, select, lookup, etc. → dimension
551
+ dimensions[fieldName] = {
552
+ name: fieldName,
553
+ label: fd.label || fieldName,
554
+ type: 'string',
555
+ sql: fieldName,
556
+ };
557
+ }
558
+ }
559
+
560
+ cubes.push({
561
+ name: schema.name,
562
+ title: schema.label || schema.name,
563
+ description: schema.description,
564
+ sql: schema.name,
565
+ measures,
566
+ dimensions,
567
+ public: true,
568
+ });
569
+ }
570
+
571
+ return {
572
+ success: true,
573
+ data: { cubes },
574
+ };
297
575
  }
298
576
 
299
- async createSpace(_request: any): Promise<any> {
300
- throw new Error('createSpace not implemented');
577
+ private mapAnalyticsOperator(op: string): string {
578
+ const map: Record<string, string> = {
579
+ equals: '$eq',
580
+ notEquals: '$ne',
581
+ contains: '$contains',
582
+ notContains: '$notContains',
583
+ gt: '$gt',
584
+ gte: '$gte',
585
+ lt: '$lt',
586
+ lte: '$lte',
587
+ set: '$ne',
588
+ notSet: '$eq',
589
+ };
590
+ return map[op] || '$eq';
301
591
  }
302
592
 
303
- async installPlugin(_request: any): Promise<any> {
304
- throw new Error('installPlugin not implemented');
593
+ async triggerAutomation(_request: any): Promise<any> {
594
+ throw new Error('triggerAutomation requires plugin-automation service. Install and register a plugin that provides the "automation" service.');
305
595
  }
306
596
 
307
597
  async deleteManyData(request: DeleteManyDataRequest): Promise<any> {
package/src/registry.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
1
3
  import { ServiceObject, ObjectSchema, ObjectOwnership } from '@objectstack/spec/data';
2
4
  import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema } from '@objectstack/spec/kernel';
3
5
  import { AppSchema } from '@objectstack/spec/ui';