@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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +20 -0
- package/dist/index.d.mts +276 -122
- package/dist/index.d.ts +276 -122
- package/dist/index.js +316 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +316 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/engine.ts +41 -15
- package/src/index.ts +2 -0
- package/src/plugin.ts +2 -0
- package/src/protocol.ts +323 -33
- package/src/registry.ts +2 -0
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
|
-
|
|
223
|
-
'
|
|
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
|
-
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
|
|
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
package/src/plugin.ts
CHANGED
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(
|
|
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:
|
|
44
|
-
analytics:
|
|
45
|
-
|
|
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
|
-
|
|
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(
|
|
83
|
+
async getMetaTypes() {
|
|
56
84
|
return {
|
|
57
85
|
types: SchemaRegistry.getRegisteredTypes()
|
|
58
86
|
};
|
|
59
87
|
}
|
|
60
88
|
|
|
61
|
-
async getMetaItems(request: { type: 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
|
|
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
|
|
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(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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(
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
|
296
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
|
304
|
-
throw new Error('
|
|
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';
|