@feasibleone/blong-gogo 1.28.0 → 1.29.0
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 +7 -0
- package/package.json +1 -1
- package/src/AdapterBase.ts +40 -13
- package/src/adapter/schema/knex/types.ts +56 -0
- package/src/adapter/server/knex.ts +437 -65
- package/template/browser.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.29.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.28.0...blong-gogo-v1.29.0) (2026-08-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* blong-access UI ([999de20](https://github.com/feasibleone/blong/commit/999de20b5f8e36979787bed71695de45a4006f5f))
|
|
9
|
+
|
|
3
10
|
## [1.28.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.27.0...blong-gogo-v1.28.0) (2026-08-19)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
package/src/AdapterBase.ts
CHANGED
|
@@ -195,14 +195,22 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
|
|
|
195
195
|
return null;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
namespaces(): (string | RegExp)[] {
|
|
199
|
+
const namespace = this.config.namespace;
|
|
200
|
+
return ([] as (string | RegExp)[]).concat(
|
|
201
|
+
namespace
|
|
202
|
+
? Array.isArray(namespace) || typeof namespace === 'string'
|
|
203
|
+
? namespace
|
|
204
|
+
: Object.keys(namespace)
|
|
205
|
+
: this.config.imports || this.config.id.replace(/\./g, '-'),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
198
209
|
handles(name: string): boolean {
|
|
199
210
|
if (reserved.includes(name)) return true;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.some(namespace =>
|
|
204
|
-
typeof namespace === 'string' ? name.startsWith(namespace) : namespace.test(name),
|
|
205
|
-
);
|
|
211
|
+
return this.namespaces().some(namespace =>
|
|
212
|
+
typeof namespace === 'string' ? name.startsWith(namespace) : namespace.test(name),
|
|
213
|
+
);
|
|
206
214
|
}
|
|
207
215
|
|
|
208
216
|
methodPath(methodName: string): string {
|
|
@@ -297,10 +305,7 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
|
|
|
297
305
|
}
|
|
298
306
|
|
|
299
307
|
forNamespaces<R>(reducer: (prev: R, current: unknown) => R, initial: R): R {
|
|
300
|
-
|
|
301
|
-
return ([] as (string | RegExp)[])
|
|
302
|
-
.concat(this.config.namespace || this.config.imports || id)
|
|
303
|
-
.reduce(reducer.bind(this), initial);
|
|
308
|
+
return this.namespaces().reduce(reducer.bind(this), initial);
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
async start(): Promise<unknown> {
|
|
@@ -390,12 +395,34 @@ export default async function adapter<T, C extends IContext>(
|
|
|
390
395
|
|
|
391
396
|
const base = new AdapterBase<T, C>(api, configBase, activationNames);
|
|
392
397
|
|
|
393
|
-
const result = handlers!({
|
|
398
|
+
const result = handlers!({
|
|
399
|
+
utError,
|
|
400
|
+
remote,
|
|
401
|
+
type,
|
|
402
|
+
schema: registry.objectSchema,
|
|
403
|
+
manifest: api.manifest,
|
|
404
|
+
});
|
|
394
405
|
let current = result;
|
|
395
406
|
while (current.extends) {
|
|
396
407
|
const parent = await (typeof current.extends === 'string'
|
|
397
|
-
? adapterFactory(current.extends)!({
|
|
398
|
-
|
|
408
|
+
? adapterFactory(current.extends)!({
|
|
409
|
+
utError,
|
|
410
|
+
remote,
|
|
411
|
+
rpc,
|
|
412
|
+
local,
|
|
413
|
+
registry,
|
|
414
|
+
schema,
|
|
415
|
+
manifest: api.manifest,
|
|
416
|
+
})
|
|
417
|
+
: current.extends({
|
|
418
|
+
utError,
|
|
419
|
+
remote,
|
|
420
|
+
rpc,
|
|
421
|
+
local,
|
|
422
|
+
registry,
|
|
423
|
+
schema,
|
|
424
|
+
manifest: api.manifest,
|
|
425
|
+
}));
|
|
399
426
|
Object.setPrototypeOf(current, parent);
|
|
400
427
|
current = parent;
|
|
401
428
|
}
|
|
@@ -40,6 +40,44 @@ export interface ITableConstraints {
|
|
|
40
40
|
>;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export interface IEdgeBinding {
|
|
44
|
+
/**
|
|
45
|
+
* The `core_triple` predicate of the edge, e.g. `hasRole`.
|
|
46
|
+
*/
|
|
47
|
+
predicate: string;
|
|
48
|
+
/**
|
|
49
|
+
* The entity table holding the edge's object rows (e.g. `access_role`).
|
|
50
|
+
* The edges themselves live in `core_triple` (`subjectId -predicate->
|
|
51
|
+
* objectId`), keyed by the resource id.
|
|
52
|
+
*/
|
|
53
|
+
table: string;
|
|
54
|
+
/**
|
|
55
|
+
* The detail object name — the sibling array key returned on `get` and
|
|
56
|
+
* accepted on `add`/`edit` (defaults to the predicate's object segment,
|
|
57
|
+
* e.g. `role` for `hasRole`).
|
|
58
|
+
*/
|
|
59
|
+
object?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The object table's PK column (defaults to `${object}Id`).
|
|
62
|
+
*/
|
|
63
|
+
objectKey?: string;
|
|
64
|
+
/**
|
|
65
|
+
* The object display-name field joined from `core_resource.resourceName`
|
|
66
|
+
* (defaults to `${object}Name`).
|
|
67
|
+
*/
|
|
68
|
+
nameField?: string;
|
|
69
|
+
/**
|
|
70
|
+
* When true, only rows with `granted !== false` are kept when syncing
|
|
71
|
+
* edges on `add`/`edit` (the model pivot submits a `granted` boolean).
|
|
72
|
+
*/
|
|
73
|
+
granted?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* When true, `remove` also deletes the reverse edges (`objectId` of this
|
|
76
|
+
* subject) pointing at this subject from other entities.
|
|
77
|
+
*/
|
|
78
|
+
reverse?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
43
81
|
export interface ISchemaTable {
|
|
44
82
|
/**
|
|
45
83
|
* The TypeBox `TObject` for the table. Optional when the definition is
|
|
@@ -48,6 +86,24 @@ export interface ISchemaTable {
|
|
|
48
86
|
*/
|
|
49
87
|
definition?: TObject;
|
|
50
88
|
order?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Marks the table as resource-backed: its PK is a FK to
|
|
91
|
+
* `core.resource.resourceId` and its display name lives in
|
|
92
|
+
* `core_resource.resourceName`. When true the generic CRUD:
|
|
93
|
+
* - `add` generates a server-side PK + `core_resource` row for a missing
|
|
94
|
+
* not-null id (and when the PK carries a `uuid`/`ulid` default marker);
|
|
95
|
+
* - `find`/`get` join `resourceName` as `${object}Name`;
|
|
96
|
+
* - `edit` renames `core_resource.resourceName` from `${object}Name`;
|
|
97
|
+
* - `remove` deletes the `core_resource` row (and the declared `edges`).
|
|
98
|
+
*/
|
|
99
|
+
resource?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Declarative graph-edge master-detail bindings (`core_triple` edges such
|
|
102
|
+
* as `hasRole` / `hasCapability`). `get` attaches the edge rows as a
|
|
103
|
+
* sibling array; `add`/`edit` diff-sync the edges (plus one
|
|
104
|
+
* `access_pathRefresh`); `remove` cleans them.
|
|
105
|
+
*/
|
|
106
|
+
edges?: IEdgeBinding[];
|
|
51
107
|
/**
|
|
52
108
|
* Optional per-table dropdown binding override used by the auto-bound
|
|
53
109
|
* `{subject}.dropdown.list` handler (see the knex adapter `exec()`).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {withProgress} from '@feasibleone/blong-lib';
|
|
1
|
+
import {ulid, withProgress} from '@feasibleone/blong-lib';
|
|
2
2
|
import {
|
|
3
3
|
adapter,
|
|
4
4
|
type Adapter,
|
|
@@ -15,8 +15,8 @@ import {type TFunction, type TObject} from 'typebox';
|
|
|
15
15
|
import {v4} from 'uuid';
|
|
16
16
|
import yaml from 'yaml';
|
|
17
17
|
import {methodParts} from '../../lib.ts';
|
|
18
|
-
import {ensureDatabase} from '../schema/knex/database.ts';
|
|
19
18
|
import {
|
|
19
|
+
binaryToStr,
|
|
20
20
|
discoverBinaryColumns,
|
|
21
21
|
isBinaryColumn,
|
|
22
22
|
prepareInputParams,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
prepareResultRows,
|
|
25
25
|
strToBinary,
|
|
26
26
|
} from '../schema/knex/binary.ts';
|
|
27
|
+
import {ensureDatabase} from '../schema/knex/database.ts';
|
|
27
28
|
import {wrapKnex} from '../schema/knex/json.ts';
|
|
28
29
|
import {
|
|
29
30
|
bindSyntheticHandlers,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
} from '../schema/knex/schemaTable.ts';
|
|
38
39
|
import {
|
|
39
40
|
type IConfig,
|
|
41
|
+
type IEdgeBinding,
|
|
40
42
|
type IKnexConfig,
|
|
41
43
|
type ISchemaTable,
|
|
42
44
|
type ITableConstraints,
|
|
@@ -152,9 +154,12 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
152
154
|
}
|
|
153
155
|
} catch (error) {
|
|
154
156
|
// Warn-and-continue: schema sync will surface the real error.
|
|
155
|
-
this.log?.warn?.(
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
this.log?.warn?.(
|
|
158
|
+
{
|
|
159
|
+
err: (error as {message?: string}).message ?? String(error),
|
|
160
|
+
},
|
|
161
|
+
'could not auto-create database — will continue',
|
|
162
|
+
);
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
this.config.context = {
|
|
@@ -367,6 +372,7 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
367
372
|
const {select: _select, ...where} = params;
|
|
368
373
|
const qb = this.config.context.queryBuilder!;
|
|
369
374
|
const binaryCols = getBinaryCols(this.config.context);
|
|
375
|
+
const opts = tableOptions(objectSchema, this.config, subject, object);
|
|
370
376
|
let query = qb(table);
|
|
371
377
|
for (const [key, val] of Object.entries(where)) {
|
|
372
378
|
if (isBinaryColumn(binaryCols, table, key) && typeof val === 'string') {
|
|
@@ -376,19 +382,34 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
376
382
|
}
|
|
377
383
|
}
|
|
378
384
|
const row = (await query.first()) as Record<string, unknown> | undefined;
|
|
379
|
-
const result: Record<string, unknown> = {
|
|
380
|
-
[object]: prepareResultRow(row, binaryCols, table),
|
|
381
|
-
};
|
|
382
|
-
// Master-detail: return each FK-constrained detail table's
|
|
383
|
-
// rows as sibling arrays (e.g. `line`, `payment`) so the
|
|
384
|
-
// Open form can render them alongside the master record.
|
|
385
385
|
const keyName =
|
|
386
386
|
(
|
|
387
387
|
objectSchema[subject]?.[object] as
|
|
388
388
|
| {constraints?: {primaryKey?: string}}
|
|
389
389
|
| undefined
|
|
390
390
|
)?.constraints?.primaryKey ?? `${object}Id`;
|
|
391
|
+
// Capture the raw PK **before** `prepareResultRow` mutates
|
|
392
|
+
// the row in place (Buffers → base64 strings). The edge
|
|
393
|
+
// attachment needs the binary master key.
|
|
391
394
|
const masterKey = row?.[keyName];
|
|
395
|
+
const result: Record<string, unknown> = {
|
|
396
|
+
[object]: prepareResultRow(row, binaryCols, table),
|
|
397
|
+
};
|
|
398
|
+
const masterRow = result[object] as Record<string, unknown> | undefined;
|
|
399
|
+
// Resource-backed: join the display name from
|
|
400
|
+
// `core_resource.resourceName` as `${object}Name`.
|
|
401
|
+
if (opts.resource && masterRow && typeof masterRow[keyName] === 'string') {
|
|
402
|
+
const [joined] = await joinResourceNames(
|
|
403
|
+
qb,
|
|
404
|
+
[masterRow],
|
|
405
|
+
keyName,
|
|
406
|
+
`${object}Name`,
|
|
407
|
+
);
|
|
408
|
+
result[object] = joined;
|
|
409
|
+
}
|
|
410
|
+
// Master-detail: return each FK-constrained detail table's
|
|
411
|
+
// rows as sibling arrays (e.g. `line`, `payment`) so the
|
|
412
|
+
// Open form can render them alongside the master record.
|
|
392
413
|
if (masterKey !== undefined) {
|
|
393
414
|
for (const detail of detailTables(subject, object, keyName)) {
|
|
394
415
|
const detailBinaryCols = getBinaryCols(this.config.context);
|
|
@@ -401,6 +422,18 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
401
422
|
detail.table,
|
|
402
423
|
);
|
|
403
424
|
}
|
|
425
|
+
// Graph-edge master-detail (declarative `edges`) — the
|
|
426
|
+
// rows live in `core_triple` keyed by the resource id.
|
|
427
|
+
// Only binary (resource-backed) master keys participate.
|
|
428
|
+
if (Buffer.isBuffer(masterKey)) {
|
|
429
|
+
for (const binding of opts.edges) {
|
|
430
|
+
if (!binding.table) continue; // reverse-only cleanup binding
|
|
431
|
+
const detailObject =
|
|
432
|
+
binding.object ??
|
|
433
|
+
binding.predicate.replace(/^has/, '').toLowerCase();
|
|
434
|
+
result[detailObject] = await attachEdgeRows(qb, masterKey, binding);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
404
437
|
}
|
|
405
438
|
return result;
|
|
406
439
|
}
|
|
@@ -418,6 +451,7 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
418
451
|
} = params;
|
|
419
452
|
const qb = this.config.context.queryBuilder!;
|
|
420
453
|
const binaryCols = getBinaryCols(this.config.context);
|
|
454
|
+
const opts = tableOptions(objectSchema, this.config, subject, object);
|
|
421
455
|
let query = qb(table);
|
|
422
456
|
for (const [key, val] of Object.entries({...filterBy, ...where})) {
|
|
423
457
|
if (isBinaryColumn(binaryCols, table, key) && typeof val === 'string') {
|
|
@@ -443,7 +477,13 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
443
477
|
if (limit) query = query.limit(limit);
|
|
444
478
|
if (offset) query = query.offset(offset);
|
|
445
479
|
const rows = (await query.select(select)) as Record<string, unknown>[];
|
|
446
|
-
|
|
480
|
+
const prepared = prepareResultRows(rows, binaryCols, table);
|
|
481
|
+
// Resource-backed: join the display name from
|
|
482
|
+
// `core_resource.resourceName` as `${object}Name`.
|
|
483
|
+
if (opts.resource) {
|
|
484
|
+
return joinResourceNames(qb, prepared, `${object}Id`, `${object}Name`);
|
|
485
|
+
}
|
|
486
|
+
return prepared;
|
|
447
487
|
}
|
|
448
488
|
case 'add': {
|
|
449
489
|
const {
|
|
@@ -452,9 +492,17 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
452
492
|
resourceName,
|
|
453
493
|
...rest
|
|
454
494
|
} = params;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
495
|
+
// Resolve the table definition from the declarative
|
|
496
|
+
// `schema.tables` entry first (falling back to the realm
|
|
497
|
+
// `objectSchema`), so namespace-scoped methods (e.g.
|
|
498
|
+
// `sql.person.add`) find their properties/FK constraints
|
|
499
|
+
// when the definition lives in the adapter table config.
|
|
500
|
+
const definition = resolveTableSpec(
|
|
501
|
+
objectSchema,
|
|
502
|
+
this.config.schema?.tables?.[`${subject}.${object}`],
|
|
503
|
+
subject,
|
|
504
|
+
object,
|
|
505
|
+
).definition as unknown as Record<string, unknown> | undefined;
|
|
458
506
|
const properties = definition?.properties as
|
|
459
507
|
| Record<string, IColumnSchema>
|
|
460
508
|
| undefined;
|
|
@@ -465,52 +513,95 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
465
513
|
)?.foreign;
|
|
466
514
|
const qb = this.config.context.queryBuilder!;
|
|
467
515
|
const binaryCols = getBinaryCols(this.config.context);
|
|
516
|
+
const opts = tableOptions(objectSchema, this.config, subject, object);
|
|
468
517
|
const cols = columns as Record<string, unknown>;
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
518
|
+
// Resource-backed: `${object}Name` is a virtual display field
|
|
519
|
+
// (the name lives in `core_resource.resourceName`) — capture
|
|
520
|
+
// it for the resource row and exclude it from the table insert.
|
|
521
|
+
const nameColValue = opts.resource
|
|
522
|
+
? (cols[`${object}Name`] as string | undefined)
|
|
523
|
+
: undefined;
|
|
524
|
+
if (opts.resource) {
|
|
525
|
+
delete cols[`${object}Name`];
|
|
526
|
+
}
|
|
527
|
+
// Generate real PKs server-side for id columns that carry a
|
|
528
|
+
// 'uuid' / 'ulid' default marker (`type.uuid()` / `type.ulid()`
|
|
529
|
+
// are submitted as the literal placeholder) OR whose not-null
|
|
530
|
+
// id the caller did not supply on a resource-backed table
|
|
531
|
+
// (PK is a FK to `core.resource`, e.g. `type.uidNotNull()`).
|
|
532
|
+
// Track the generated key so the inserted row can be selected
|
|
533
|
+
// back by it.
|
|
472
534
|
let generatedKey: string | undefined;
|
|
535
|
+
// Ensure a `core_type` row exists (mirrors core.resource.ensure)
|
|
536
|
+
// so resource-backed inserts always get a type.
|
|
537
|
+
const ensureType = async (typeAlias: string): Promise<number | undefined> => {
|
|
538
|
+
const existing = await qb('core_type').where({typeAlias}).first('typeId');
|
|
539
|
+
if (existing) return existing.typeId as number;
|
|
540
|
+
await qb('core_type').insert({typeAlias}).onConflict().ignore();
|
|
541
|
+
const inserted = await qb('core_type').where({typeAlias}).first('typeId');
|
|
542
|
+
return inserted ? (inserted.typeId as number) : undefined;
|
|
543
|
+
};
|
|
544
|
+
// Create the matching `core_resource` row for a generated
|
|
545
|
+
// resource-backed PK. The name is the entity's display label
|
|
546
|
+
// in `{subject}.dropdown.list`.
|
|
547
|
+
const ensureResourceRow = async (
|
|
548
|
+
idStr: string,
|
|
549
|
+
resourceKeyCol: string,
|
|
550
|
+
): Promise<void> => {
|
|
551
|
+
const typeAlias = `${subject}.${object}`;
|
|
552
|
+
const typeId = await ensureType(typeAlias);
|
|
553
|
+
if (!typeId) return;
|
|
554
|
+
const name =
|
|
555
|
+
(typeof resourceName === 'string' && resourceName) ||
|
|
556
|
+
nameColValue ||
|
|
557
|
+
`${subject}.${object}.${resourceKeyCol}`;
|
|
558
|
+
await qb('core_resource')
|
|
559
|
+
.insert({
|
|
560
|
+
resourceId: strToBinary(idStr),
|
|
561
|
+
resourceName: name,
|
|
562
|
+
typeId,
|
|
563
|
+
})
|
|
564
|
+
.onConflict()
|
|
565
|
+
.ignore();
|
|
566
|
+
};
|
|
567
|
+
// 1) Literal 'uuid' / 'ulid' default markers on id columns.
|
|
473
568
|
for (const colName of Object.keys(cols)) {
|
|
474
|
-
if (cols[colName] !== 'uuid') continue;
|
|
569
|
+
if (cols[colName] !== 'uuid' && cols[colName] !== 'ulid') continue;
|
|
475
570
|
const prop = properties?.[colName];
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
// corresponding core_resource row.
|
|
571
|
+
const marker = cols[colName] as 'uuid' | 'ulid';
|
|
572
|
+
if (!prop || propDefault(prop) !== marker) continue;
|
|
573
|
+
const idStr = marker === 'ulid' ? ulid() : crypto.randomUUID();
|
|
574
|
+
cols[colName] = strToBinary(idStr);
|
|
575
|
+
generatedKey = idStr;
|
|
482
576
|
if (foreignKeys?.[colName] === 'core.resource.resourceId') {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
`${subject}.${object}.${colName}`;
|
|
499
|
-
await qb('core_resource')
|
|
500
|
-
.insert({
|
|
501
|
-
resourceId: strToBinary(uuidStr),
|
|
502
|
-
resourceName: name,
|
|
503
|
-
typeId: typeRow.typeId,
|
|
504
|
-
})
|
|
505
|
-
.onConflict()
|
|
506
|
-
.ignore();
|
|
507
|
-
}
|
|
577
|
+
await ensureResourceRow(idStr, colName);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// 2) Resource-backed not-null PK (no default marker, e.g.
|
|
581
|
+
// `type.uidNotNull()`) with no key supplied by the caller.
|
|
582
|
+
if (
|
|
583
|
+
!generatedKey &&
|
|
584
|
+
cols[keyName] == null &&
|
|
585
|
+
foreignKeys?.[keyName] === 'core.resource.resourceId'
|
|
586
|
+
) {
|
|
587
|
+
const pkProp = properties?.[keyName];
|
|
588
|
+
if (!pkProp || !propDefault(pkProp)) {
|
|
589
|
+
generatedKey = crypto.randomUUID();
|
|
590
|
+
cols[keyName] = strToBinary(generatedKey);
|
|
591
|
+
await ensureResourceRow(generatedKey, keyName);
|
|
508
592
|
}
|
|
509
593
|
}
|
|
510
594
|
// Convert any string values for binary columns to Buffer
|
|
511
595
|
const insertCols = prepareInputParams(cols, binaryCols, table);
|
|
512
596
|
const inserted = await qb(table).insert(insertCols);
|
|
513
|
-
|
|
597
|
+
// Select the inserted row back by the PK. Prefer the explicit
|
|
598
|
+
// key value (post-conversion) when the caller supplied one
|
|
599
|
+
// (e.g. a real ULID/UUID string) — `insertId` only works for
|
|
600
|
+
// auto-increment PKs and is 0 for a binary-PK table.
|
|
601
|
+
const masterKey =
|
|
602
|
+
generatedKey
|
|
603
|
+
? strToBinary(generatedKey)
|
|
604
|
+
: (insertCols[keyName] as Buffer | string | undefined) ?? inserted[0];
|
|
514
605
|
const row = (await qb(table)
|
|
515
606
|
.where({[keyName]: masterKey})
|
|
516
607
|
.first()) as Record<string, unknown>;
|
|
@@ -548,16 +639,69 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
548
639
|
detail.table,
|
|
549
640
|
);
|
|
550
641
|
}
|
|
642
|
+
// Graph-edge master-detail: persist each declared edge from
|
|
643
|
+
// its sibling array (filtering `granted !== false` when the
|
|
644
|
+
// binding uses the pivot convention) and attach the rows.
|
|
645
|
+
if (Buffer.isBuffer(masterKey)) {
|
|
646
|
+
const opts = tableOptions(objectSchema, this.config, subject, object);
|
|
647
|
+
for (const binding of opts.edges) {
|
|
648
|
+
if (!binding.table) continue; // reverse-only cleanup binding
|
|
649
|
+
const detailObject =
|
|
650
|
+
binding.object ??
|
|
651
|
+
binding.predicate.replace(/^has/, '').toLowerCase();
|
|
652
|
+
const edgeRows = Array.isArray(rest[detailObject])
|
|
653
|
+
? (rest[detailObject] as Array<Record<string, unknown>>)
|
|
654
|
+
: [];
|
|
655
|
+
const objectKey = binding.objectKey ?? `${detailObject}Id`;
|
|
656
|
+
const ids = edgeRows
|
|
657
|
+
.filter(r => (binding.granted ? r.granted !== false : true))
|
|
658
|
+
.map(r => {
|
|
659
|
+
const id = r[objectKey];
|
|
660
|
+
return typeof id === 'string'
|
|
661
|
+
? strToBinary(id).toString('hex')
|
|
662
|
+
: undefined;
|
|
663
|
+
})
|
|
664
|
+
.filter((x): x is string => !!x);
|
|
665
|
+
if (ids.length) {
|
|
666
|
+
await syncGraphEdges(qb, masterKey, binding.predicate, ids);
|
|
667
|
+
}
|
|
668
|
+
result[detailObject] = await attachEdgeRows(qb, masterKey, binding);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// Resource-backed: join the display name onto the master so
|
|
672
|
+
// the caller sees `${object}Name` in the created row.
|
|
673
|
+
if (opts.resource && Buffer.isBuffer(masterKey)) {
|
|
674
|
+
const masterRow = result[object] as Record<string, unknown> | undefined;
|
|
675
|
+
if (masterRow && typeof masterRow[`${object}Id`] === 'string') {
|
|
676
|
+
const [joined] = await joinResourceNames(
|
|
677
|
+
qb,
|
|
678
|
+
[masterRow],
|
|
679
|
+
`${object}Id`,
|
|
680
|
+
`${object}Name`,
|
|
681
|
+
);
|
|
682
|
+
result[object] = joined;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
551
685
|
return result;
|
|
552
686
|
}
|
|
553
687
|
case 'edit': {
|
|
554
688
|
const {key: keyName = `${object}Id`, [object]: columns, ...rest} = params;
|
|
555
689
|
const qb = this.config.context.queryBuilder!;
|
|
556
690
|
const binaryCols = getBinaryCols(this.config.context);
|
|
691
|
+
const opts = tableOptions(objectSchema, this.config, subject, object);
|
|
557
692
|
const cols = columns as Record<string, unknown>;
|
|
558
693
|
const {[keyName]: key, ...update} = cols as Record<string, unknown>;
|
|
559
694
|
const isBinaryKey =
|
|
560
695
|
isBinaryColumn(binaryCols, table, keyName) && typeof key === 'string';
|
|
696
|
+
// Resource-backed: `${object}Name` is a virtual field — the
|
|
697
|
+
// display name lives in `core_resource.resourceName`, so
|
|
698
|
+
// rename that row instead of updating a table column.
|
|
699
|
+
const resourceName = opts.resource
|
|
700
|
+
? (update[`${object}Name`] as string | undefined)
|
|
701
|
+
: undefined;
|
|
702
|
+
if (opts.resource) {
|
|
703
|
+
delete update[`${object}Name`];
|
|
704
|
+
}
|
|
561
705
|
// Convert any string values for binary columns to Buffer (the
|
|
562
706
|
// form round-trips them as base64 strings returned by `get`).
|
|
563
707
|
const preparedUpdate = prepareInputParams(update, binaryCols, table);
|
|
@@ -569,6 +713,16 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
569
713
|
.where({[keyName]: key})
|
|
570
714
|
.update(preparedUpdate);
|
|
571
715
|
}
|
|
716
|
+
if (
|
|
717
|
+
opts.resource &&
|
|
718
|
+
isBinaryKey &&
|
|
719
|
+
typeof resourceName === 'string' &&
|
|
720
|
+
resourceName
|
|
721
|
+
) {
|
|
722
|
+
await qb('core_resource')
|
|
723
|
+
.where('resourceId', strToBinary(key))
|
|
724
|
+
.update({resourceName});
|
|
725
|
+
}
|
|
572
726
|
// Select back with Buffer → base64 conversion
|
|
573
727
|
let editQuery = qb(table);
|
|
574
728
|
if (isBinaryKey) {
|
|
@@ -603,7 +757,39 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
603
757
|
);
|
|
604
758
|
}
|
|
605
759
|
}
|
|
606
|
-
|
|
760
|
+
const result: Record<string, unknown> = {
|
|
761
|
+
[object]: prepareResultRow(editRow, binaryCols, table),
|
|
762
|
+
};
|
|
763
|
+
// Graph-edge master-detail: bring each declared edge in line
|
|
764
|
+
// with the submitted sibling array (when present) and re-attach
|
|
765
|
+
// the fresh edge rows to the result.
|
|
766
|
+
if (isBinaryKey) {
|
|
767
|
+
const masterKeyBuf = strToBinary(key);
|
|
768
|
+
for (const binding of opts.edges) {
|
|
769
|
+
if (!binding.table) continue; // reverse-only cleanup binding
|
|
770
|
+
const detailObject =
|
|
771
|
+
binding.object ??
|
|
772
|
+
binding.predicate.replace(/^has/, '').toLowerCase();
|
|
773
|
+
const edgeRows = Array.isArray(rest[detailObject])
|
|
774
|
+
? (rest[detailObject] as Array<Record<string, unknown>>)
|
|
775
|
+
: undefined;
|
|
776
|
+
if (edgeRows !== undefined) {
|
|
777
|
+
const objectKey = binding.objectKey ?? `${detailObject}Id`;
|
|
778
|
+
const ids = edgeRows
|
|
779
|
+
.filter(r => (binding.granted ? r.granted !== false : true))
|
|
780
|
+
.map(r => {
|
|
781
|
+
const id = r[objectKey];
|
|
782
|
+
return typeof id === 'string'
|
|
783
|
+
? strToBinary(id).toString('hex')
|
|
784
|
+
: undefined;
|
|
785
|
+
})
|
|
786
|
+
.filter((x): x is string => !!x);
|
|
787
|
+
await syncGraphEdges(qb, masterKeyBuf, binding.predicate, ids);
|
|
788
|
+
}
|
|
789
|
+
result[detailObject] = await attachEdgeRows(qb, masterKeyBuf, binding);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return result;
|
|
607
793
|
}
|
|
608
794
|
case 'remove': {
|
|
609
795
|
const {key: keyName = `${object}Id`, [keyName]: key} = params;
|
|
@@ -611,28 +797,54 @@ export default adapter<IConfig>(({utError, schema: objectSchema}) => {
|
|
|
611
797
|
throw this.error(_errors['knex.missingKey']({key: keyName}), $meta);
|
|
612
798
|
}
|
|
613
799
|
const binaryCols = getBinaryCols(this.config.context);
|
|
800
|
+
const opts = tableOptions(objectSchema, this.config, subject, object);
|
|
614
801
|
const isBinaryKey =
|
|
615
802
|
isBinaryColumn(binaryCols, table, keyName) && typeof key === 'string';
|
|
616
803
|
const masterKey = isBinaryKey ? strToBinary(key as string) : key;
|
|
617
804
|
if (!masterKey) {
|
|
618
805
|
throw this.error(_errors['knex.missingKey']({key: keyName}), $meta);
|
|
619
806
|
}
|
|
807
|
+
const qb = this.config.context.queryBuilder!;
|
|
620
808
|
// Master-detail: delete each FK-constrained detail table's
|
|
621
809
|
// rows for this master BEFORE deleting the master row, so a
|
|
622
810
|
// non-cascading FK does not block the delete.
|
|
623
811
|
for (const detail of detailTables(subject, object, keyName)) {
|
|
624
|
-
await
|
|
812
|
+
await qb(detail.table)
|
|
625
813
|
.where({[detail.fkColumn]: masterKey})
|
|
626
814
|
.del();
|
|
627
815
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
816
|
+
// Graph edges: delete the subject's own edges and (when the
|
|
817
|
+
// binding declares `reverse`) the edges pointing AT it.
|
|
818
|
+
if (Buffer.isBuffer(masterKey)) {
|
|
819
|
+
const subjectBuf = masterKey as Buffer;
|
|
820
|
+
for (const binding of opts.edges) {
|
|
821
|
+
await qb('core_triple')
|
|
822
|
+
.where('subjectId', subjectBuf)
|
|
823
|
+
.where('predicateName', binding.predicate)
|
|
824
|
+
.del();
|
|
825
|
+
if (binding.reverse) {
|
|
826
|
+
await qb('core_triple')
|
|
827
|
+
.where('objectId', subjectBuf)
|
|
828
|
+
.where('predicateName', binding.predicate)
|
|
829
|
+
.del();
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (opts.edges.length) {
|
|
833
|
+
await qb.raw('CALL access_pathRefresh()');
|
|
834
|
+
}
|
|
632
835
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
836
|
+
// Entity row first — its PK is a FK to `core_resource`, so
|
|
837
|
+
// the resource row must be deleted only after the entity row.
|
|
838
|
+
const removed = isBinaryKey
|
|
839
|
+
? await qb(table).where(keyName, masterKey).del()
|
|
840
|
+
: await qb(table)
|
|
841
|
+
.where({[keyName]: key})
|
|
842
|
+
.del();
|
|
843
|
+
// Resource-backed: delete the `core_resource` row last.
|
|
844
|
+
if (Buffer.isBuffer(masterKey) && opts.resource) {
|
|
845
|
+
await qb('core_resource').where('resourceId', masterKey).del();
|
|
846
|
+
}
|
|
847
|
+
return removed;
|
|
636
848
|
}
|
|
637
849
|
case 'merge': {
|
|
638
850
|
const {key = `${object}Id`, [object]: objectRows, resourceType} = params;
|
|
@@ -936,21 +1148,35 @@ async function processSeedAssets(
|
|
|
936
1148
|
|
|
937
1149
|
export {attachHandlers, methodId, snakeToCamel};
|
|
938
1150
|
|
|
1151
|
+
/** Resolved per-table CRUD options (resource-backed + graph-edge bindings). */
|
|
1152
|
+
export interface IResolvedTableOptions {
|
|
1153
|
+
/** Whether the table is resource-backed (PK → `core.resource`). */
|
|
1154
|
+
resource: boolean;
|
|
1155
|
+
/** Declarative graph-edge master-detail bindings. */
|
|
1156
|
+
edges: IEdgeBinding[];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
939
1159
|
/**
|
|
940
1160
|
* Resolve a `schema.tables` entry into its definition + dropdown override.
|
|
941
1161
|
*
|
|
942
1162
|
* A table entry is one of:
|
|
943
1163
|
* - a plain order number → definition from `objectSchema[subject][object]`
|
|
944
|
-
* - an `ISchemaTable` spec `{definition?, order?, dropdown?}`
|
|
945
|
-
* falls back to `objectSchema[subject][object]`
|
|
1164
|
+
* - an `ISchemaTable` spec `{definition?, order?, dropdown?, resource?, edges?}`
|
|
1165
|
+
* — definition falls back to `objectSchema[subject][object]`
|
|
946
1166
|
* - a bare TypeBox `TObject`
|
|
947
1167
|
*/
|
|
948
1168
|
function resolveTableSpec(
|
|
949
1169
|
objectSchema: IObjectSchema,
|
|
950
|
-
tableConfig: number | ISchemaTable | TObject,
|
|
1170
|
+
tableConfig: number | ISchemaTable | TObject | undefined,
|
|
951
1171
|
subject: string,
|
|
952
1172
|
object: string,
|
|
953
|
-
): {
|
|
1173
|
+
): {
|
|
1174
|
+
definition?: TObject;
|
|
1175
|
+
dropdown?: ISchemaTable['dropdown'];
|
|
1176
|
+
resource?: boolean;
|
|
1177
|
+
edges?: IEdgeBinding[];
|
|
1178
|
+
} {
|
|
1179
|
+
if (tableConfig === undefined) return {};
|
|
954
1180
|
if (typeof tableConfig === 'number') {
|
|
955
1181
|
return {definition: objectSchema[subject]?.[object]};
|
|
956
1182
|
}
|
|
@@ -958,14 +1184,160 @@ function resolveTableSpec(
|
|
|
958
1184
|
// An ISchemaTable spec — either with an explicit `definition`, or a
|
|
959
1185
|
// partial spec (e.g. only `{order, dropdown}`) that falls back to the
|
|
960
1186
|
// realm schema for its definition.
|
|
961
|
-
if (
|
|
1187
|
+
if (
|
|
1188
|
+
'definition' in tableConfig ||
|
|
1189
|
+
'order' in tableConfig ||
|
|
1190
|
+
'dropdown' in tableConfig ||
|
|
1191
|
+
'resource' in tableConfig ||
|
|
1192
|
+
'edges' in tableConfig
|
|
1193
|
+
) {
|
|
962
1194
|
const spec = tableConfig as ISchemaTable;
|
|
963
1195
|
return {
|
|
964
1196
|
definition: spec.definition ?? objectSchema[subject]?.[object],
|
|
965
1197
|
dropdown: spec.dropdown,
|
|
1198
|
+
resource: spec.resource,
|
|
1199
|
+
edges: spec.edges,
|
|
966
1200
|
};
|
|
967
1201
|
}
|
|
968
1202
|
return {definition: tableConfig as TObject};
|
|
969
1203
|
}
|
|
970
1204
|
return {};
|
|
971
1205
|
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Resolve the declarative CRUD options for `subject.object` from the schema
|
|
1209
|
+
* table config. Tables declared with `resource: true` (or an `edges` binding)
|
|
1210
|
+
* get the resource-backed + graph-edge generic behaviour.
|
|
1211
|
+
*/
|
|
1212
|
+
function tableOptions(
|
|
1213
|
+
objectSchema: IObjectSchema,
|
|
1214
|
+
config: {schema?: {tables?: Record<string, number | ISchemaTable | TObject>}} | undefined,
|
|
1215
|
+
subject: string,
|
|
1216
|
+
object: string,
|
|
1217
|
+
): IResolvedTableOptions {
|
|
1218
|
+
const tableConfig = config?.schema?.tables?.[`${subject}.${object}`];
|
|
1219
|
+
const spec =
|
|
1220
|
+
tableConfig !== undefined
|
|
1221
|
+
? resolveTableSpec(objectSchema, tableConfig, subject, object)
|
|
1222
|
+
: {};
|
|
1223
|
+
const edges = spec.edges ?? [];
|
|
1224
|
+
return {
|
|
1225
|
+
resource: spec.resource === true || edges.length > 0,
|
|
1226
|
+
edges,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Batched join of `core_resource.resourceName` onto rows as the given name
|
|
1232
|
+
* field (e.g. `roleName`). Row ids are base64/hex strings (post
|
|
1233
|
+
* `prepareResultRow`); rows that already carry the name field are left as-is.
|
|
1234
|
+
*/
|
|
1235
|
+
async function joinResourceNames(
|
|
1236
|
+
qb: Knex,
|
|
1237
|
+
rows: Record<string, unknown>[],
|
|
1238
|
+
idField: string,
|
|
1239
|
+
nameField: string,
|
|
1240
|
+
): Promise<Record<string, unknown>[]> {
|
|
1241
|
+
if (!rows.length) return rows;
|
|
1242
|
+
const ids = rows
|
|
1243
|
+
.map(r => (typeof r[idField] === 'string' ? (r[idField] as string) : undefined))
|
|
1244
|
+
.filter((x): x is string => !!x);
|
|
1245
|
+
if (!ids.length) return rows;
|
|
1246
|
+
const found = (await qb('core_resource')
|
|
1247
|
+
.whereIn(
|
|
1248
|
+
'resourceId',
|
|
1249
|
+
ids.map(id => strToBinary(id)),
|
|
1250
|
+
)
|
|
1251
|
+
.select('resourceId', 'resourceName')) as Array<{resourceId: Buffer; resourceName: string}>;
|
|
1252
|
+
const names = new Map<string, string>();
|
|
1253
|
+
for (const r of found) names.set(r.resourceId.toString('hex'), r.resourceName);
|
|
1254
|
+
return rows.map(row => {
|
|
1255
|
+
if (row[nameField] !== undefined) return row;
|
|
1256
|
+
const id = typeof row[idField] === 'string' ? (row[idField] as string) : undefined;
|
|
1257
|
+
if (!id) return row;
|
|
1258
|
+
const name = names.get(strToBinary(id).toString('hex'));
|
|
1259
|
+
return name !== undefined ? ({...row, [nameField]: name} as Record<string, unknown>) : row;
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* The hex object ids of a subject's graph edges (`core_triple`).
|
|
1265
|
+
*/
|
|
1266
|
+
async function edgeObjectIds(qb: Knex, subjectId: Buffer, predicate: string): Promise<string[]> {
|
|
1267
|
+
const rows = (await qb('core_triple')
|
|
1268
|
+
.where('subjectId', subjectId)
|
|
1269
|
+
.where('predicateName', predicate)
|
|
1270
|
+
.select('objectId')) as Array<{objectId: Buffer}>;
|
|
1271
|
+
return rows.map(r => r.objectId.toString('hex'));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Bring `subjectId -predicate-> objectId` edges in line with `objectHexIds`
|
|
1276
|
+
* (add missing, delete stale, refresh `access_path` once).
|
|
1277
|
+
*/
|
|
1278
|
+
async function syncGraphEdges(
|
|
1279
|
+
qb: Knex,
|
|
1280
|
+
subjectId: Buffer,
|
|
1281
|
+
predicate: string,
|
|
1282
|
+
objectHexIds: string[],
|
|
1283
|
+
): Promise<void> {
|
|
1284
|
+
const existing = await edgeObjectIds(qb, subjectId, predicate);
|
|
1285
|
+
const existingSet = new Set(existing);
|
|
1286
|
+
const target = new Set(objectHexIds);
|
|
1287
|
+
const toAdd = objectHexIds.filter(id => !existingSet.has(id));
|
|
1288
|
+
const toRemove = existing.filter(id => !target.has(id));
|
|
1289
|
+
if (!toAdd.length && !toRemove.length) return;
|
|
1290
|
+
await qb.transaction(async trx => {
|
|
1291
|
+
if (toAdd.length) {
|
|
1292
|
+
await trx('core_triple').insert(
|
|
1293
|
+
toAdd.map(objectId => ({
|
|
1294
|
+
subjectId,
|
|
1295
|
+
predicateName: predicate,
|
|
1296
|
+
objectId: Buffer.from(objectId, 'hex'),
|
|
1297
|
+
})),
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
if (toRemove.length) {
|
|
1301
|
+
await trx('core_triple')
|
|
1302
|
+
.where('subjectId', subjectId)
|
|
1303
|
+
.where('predicateName', predicate)
|
|
1304
|
+
.whereIn(
|
|
1305
|
+
'objectId',
|
|
1306
|
+
toRemove.map(id => Buffer.from(id, 'hex')),
|
|
1307
|
+
)
|
|
1308
|
+
.del();
|
|
1309
|
+
}
|
|
1310
|
+
await trx.raw('CALL access_pathRefresh()');
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* Attach a graph-edge binding's rows to a result as a sibling array: the edge
|
|
1316
|
+
* object rows joined with their resource name (and a `granted: true` marker
|
|
1317
|
+
* when the binding uses the `granted` pivot convention).
|
|
1318
|
+
*/
|
|
1319
|
+
async function attachEdgeRows(
|
|
1320
|
+
qb: Knex,
|
|
1321
|
+
subjectId: Buffer,
|
|
1322
|
+
binding: IEdgeBinding,
|
|
1323
|
+
): Promise<Record<string, unknown>[]> {
|
|
1324
|
+
const object = binding.object ?? binding.predicate.replace(/^has/, '').toLowerCase();
|
|
1325
|
+
const objectKey = binding.objectKey ?? `${object}Id`;
|
|
1326
|
+
const nameField = binding.nameField ?? `${object}Name`;
|
|
1327
|
+
const ids = await edgeObjectIds(qb, subjectId, binding.predicate);
|
|
1328
|
+
if (!ids.length) return [];
|
|
1329
|
+
const rows = (await qb(binding.table)
|
|
1330
|
+
.whereIn(
|
|
1331
|
+
objectKey,
|
|
1332
|
+
ids.map(hex => Buffer.from(hex, 'hex')),
|
|
1333
|
+
)
|
|
1334
|
+
.select('*')) as Record<string, unknown>[];
|
|
1335
|
+
// Prepare binary keys to base64 before joining names — joinResourceNames
|
|
1336
|
+
// expects string ids (base64/hex), not Buffers.
|
|
1337
|
+
const prepared = rows.map(row => ({
|
|
1338
|
+
...row,
|
|
1339
|
+
[objectKey]: binaryToStr(row[objectKey] as Buffer),
|
|
1340
|
+
}));
|
|
1341
|
+
const named = await joinResourceNames(qb, prepared, objectKey, nameField);
|
|
1342
|
+
return named.map(row => (binding.granted ? {...row, granted: true} : row));
|
|
1343
|
+
}
|
package/template/browser.ts
CHANGED
|
@@ -17,8 +17,8 @@ import {realm} from '@feasibleone/blong';
|
|
|
17
17
|
export default realm(() => ({
|
|
18
18
|
url: import.meta.url,
|
|
19
19
|
children: globalThis.window
|
|
20
|
-
? import.meta.glob(['./meta/**/*.ts', './browser/orchestrator/**/*.ts'])
|
|
21
|
-
: ['./meta', './browser/orchestrator'],
|
|
20
|
+
? import.meta.glob(['./meta/model/**/*.ts', './browser/orchestrator/**/*.ts'])
|
|
21
|
+
: ['./meta/model', './browser/orchestrator'],
|
|
22
22
|
config: {
|
|
23
23
|
default: {
|
|
24
24
|
meta: true,
|