@weave-kit/engine 1.4.0 → 1.5.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 +63 -0
- package/README.md +15 -8
- package/dist/adapters/graphql/resolvers.d.ts +3 -2
- package/dist/adapters/graphql/resolvers.d.ts.map +1 -1
- package/dist/adapters/graphql/resolvers.js +1 -0
- package/dist/adapters/graphql/resolvers.js.map +1 -1
- package/dist/adapters/graphql/schema.d.ts.map +1 -1
- package/dist/adapters/graphql/schema.js +11 -1
- package/dist/adapters/graphql/schema.js.map +1 -1
- package/dist/adapters/mcp/generate.d.ts +0 -1
- package/dist/adapters/mcp/generate.d.ts.map +1 -1
- package/dist/adapters/mcp/generate.js +2 -12
- package/dist/adapters/mcp/generate.js.map +1 -1
- package/dist/adapters/mcp/tools.d.ts.map +1 -1
- package/dist/adapters/mcp/tools.js +2 -0
- package/dist/adapters/mcp/tools.js.map +1 -1
- package/dist/adapters/openapi/document.d.ts.map +1 -1
- package/dist/adapters/openapi/document.js +4 -3
- package/dist/adapters/openapi/document.js.map +1 -1
- package/dist/adapters/rest/plugin.d.ts.map +1 -1
- package/dist/adapters/rest/plugin.js +8 -1
- package/dist/adapters/rest/plugin.js.map +1 -1
- package/dist/cli/commands/deploy.d.ts.map +1 -1
- package/dist/cli/commands/deploy.js +3 -0
- package/dist/cli/commands/deploy.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +64 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +3 -0
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/project-types/manifest.d.ts +7 -0
- package/dist/cli/project-types/manifest.d.ts.map +1 -1
- package/dist/cli/project-types/manifest.js +4 -0
- package/dist/cli/project-types/manifest.js.map +1 -1
- package/dist/cli/scaffold.d.ts.map +1 -1
- package/dist/cli/scaffold.js +33 -7
- package/dist/cli/scaffold.js.map +1 -1
- package/dist/core/api/params.d.ts.map +1 -1
- package/dist/core/api/params.js +3 -0
- package/dist/core/api/params.js.map +1 -1
- package/dist/core/api/types.d.ts +4 -1
- package/dist/core/api/types.d.ts.map +1 -1
- package/dist/core/i18n/ar.d.ts.map +1 -1
- package/dist/core/i18n/ar.js +1 -0
- package/dist/core/i18n/ar.js.map +1 -1
- package/dist/core/i18n/de.d.ts.map +1 -1
- package/dist/core/i18n/de.js +1 -0
- package/dist/core/i18n/de.js.map +1 -1
- package/dist/core/i18n/en.d.ts +1 -0
- package/dist/core/i18n/en.d.ts.map +1 -1
- package/dist/core/i18n/en.js +1 -0
- package/dist/core/i18n/en.js.map +1 -1
- package/dist/core/i18n/es.d.ts.map +1 -1
- package/dist/core/i18n/es.js +1 -0
- package/dist/core/i18n/es.js.map +1 -1
- package/dist/core/i18n/fr.d.ts.map +1 -1
- package/dist/core/i18n/fr.js +1 -0
- package/dist/core/i18n/fr.js.map +1 -1
- package/dist/core/i18n/hi.d.ts.map +1 -1
- package/dist/core/i18n/hi.js +1 -0
- package/dist/core/i18n/hi.js.map +1 -1
- package/dist/core/i18n/id.d.ts.map +1 -1
- package/dist/core/i18n/id.js +1 -0
- package/dist/core/i18n/id.js.map +1 -1
- package/dist/core/i18n/ja.d.ts.map +1 -1
- package/dist/core/i18n/ja.js +1 -0
- package/dist/core/i18n/ja.js.map +1 -1
- package/dist/core/i18n/ko.d.ts.map +1 -1
- package/dist/core/i18n/ko.js +1 -0
- package/dist/core/i18n/ko.js.map +1 -1
- package/dist/core/i18n/pt.d.ts.map +1 -1
- package/dist/core/i18n/pt.js +1 -0
- package/dist/core/i18n/pt.js.map +1 -1
- package/dist/core/i18n/ru.d.ts.map +1 -1
- package/dist/core/i18n/ru.js +1 -0
- package/dist/core/i18n/ru.js.map +1 -1
- package/dist/core/i18n/vi.d.ts.map +1 -1
- package/dist/core/i18n/vi.js +1 -0
- package/dist/core/i18n/vi.js.map +1 -1
- package/dist/core/i18n/zh-hant.d.ts.map +1 -1
- package/dist/core/i18n/zh-hant.js +1 -0
- package/dist/core/i18n/zh-hant.js.map +1 -1
- package/dist/core/i18n/zh.d.ts.map +1 -1
- package/dist/core/i18n/zh.js +1 -0
- package/dist/core/i18n/zh.js.map +1 -1
- package/dist/core/rbac/index.d.ts +1 -1
- package/dist/core/rbac/index.d.ts.map +1 -1
- package/dist/core/rbac/index.js +1 -1
- package/dist/core/rbac/index.js.map +1 -1
- package/dist/core/rbac/rls.d.ts +34 -0
- package/dist/core/rbac/rls.d.ts.map +1 -1
- package/dist/core/rbac/rls.js +110 -0
- package/dist/core/rbac/rls.js.map +1 -1
- package/dist/core/storage/diff.d.ts +9 -0
- package/dist/core/storage/diff.d.ts.map +1 -1
- package/dist/core/storage/diff.js +47 -5
- package/dist/core/storage/diff.js.map +1 -1
- package/dist/core/storage/index.d.ts +2 -1
- package/dist/core/storage/index.d.ts.map +1 -1
- package/dist/core/storage/index.js +2 -1
- package/dist/core/storage/index.js.map +1 -1
- package/dist/core/storage/inspect.d.ts +4 -0
- package/dist/core/storage/inspect.d.ts.map +1 -1
- package/dist/core/storage/inspect.js +16 -8
- package/dist/core/storage/inspect.js.map +1 -1
- package/dist/core/storage/migrate.d.ts +8 -0
- package/dist/core/storage/migrate.d.ts.map +1 -1
- package/dist/core/storage/migrate.js +19 -0
- package/dist/core/storage/migrate.js.map +1 -1
- package/dist/core/storage/object-rls.d.ts +23 -0
- package/dist/core/storage/object-rls.d.ts.map +1 -0
- package/dist/core/storage/object-rls.js +44 -0
- package/dist/core/storage/object-rls.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/data-access/builder.d.ts +7 -0
- package/dist/runtime/data-access/builder.d.ts.map +1 -1
- package/dist/runtime/data-access/builder.js +12 -0
- package/dist/runtime/data-access/builder.js.map +1 -1
- package/dist/runtime/data-access/index.d.ts +3 -3
- package/dist/runtime/data-access/index.d.ts.map +1 -1
- package/dist/runtime/data-access/index.js +2 -2
- package/dist/runtime/data-access/index.js.map +1 -1
- package/dist/runtime/data-access/query.d.ts +49 -7
- package/dist/runtime/data-access/query.d.ts.map +1 -1
- package/dist/runtime/data-access/query.js +235 -113
- package/dist/runtime/data-access/query.js.map +1 -1
- package/dist/runtime/data-access/rls-session.d.ts +24 -0
- package/dist/runtime/data-access/rls-session.d.ts.map +1 -0
- package/dist/runtime/data-access/rls-session.js +30 -0
- package/dist/runtime/data-access/rls-session.js.map +1 -0
- package/dist/runtime/data-access/sql-executor.d.ts.map +1 -1
- package/dist/runtime/data-access/sql-executor.js +2 -16
- package/dist/runtime/data-access/sql-executor.js.map +1 -1
- package/dist/runtime/data-access/types.d.ts +10 -2
- package/dist/runtime/data-access/types.d.ts.map +1 -1
- package/dist/runtime/data-access/types.js.map +1 -1
- package/dist/runtime/data-access/values.d.ts +10 -0
- package/dist/runtime/data-access/values.d.ts.map +1 -1
- package/dist/runtime/data-access/values.js +9 -0
- package/dist/runtime/data-access/values.js.map +1 -1
- package/dist/runtime/engine.d.ts +35 -4
- package/dist/runtime/engine.d.ts.map +1 -1
- package/dist/runtime/engine.js +32 -3
- package/dist/runtime/engine.js.map +1 -1
- package/dist/runtime/git/sync.d.ts +4 -0
- package/dist/runtime/git/sync.d.ts.map +1 -1
- package/dist/runtime/git/sync.js +1 -0
- package/dist/runtime/git/sync.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import { SchemaError, METRIC_NAMES, primaryFieldsOf, encodeRecordKey, decodeRecordKey, canonicalizePrimaryValue, principalActorId } from '../../core/index.js';
|
|
1
|
+
import { SchemaError, METRIC_NAMES, isSafeRlsRole, primaryFieldsOf, encodeRecordKey, decodeRecordKey, canonicalizePrimaryValue, principalActorId } from '../../core/index.js';
|
|
2
2
|
import { DETAILS_COLUMNS, FIELD_TYPES, isSideTableVirtualField, RECORD_META_ID_FIELD, recordKeySql, recordMetaTableName, scopeFieldName } from '../../core/index.js';
|
|
3
3
|
import { AUDIT_ACTOR_TYPES, DATA_ACTIONS } from '../../core/audit/index.js';
|
|
4
4
|
import { NOOP_SCRIPT_DISPATCHER, SCRIPT_HOOKS } from '../../core/index.js';
|
|
5
5
|
import { evaluateTransition } from '../tools/policies.js';
|
|
6
|
-
import { buildCountSql, buildFindSql, resolvePagination, scopeSuffix } from './builder.js';
|
|
6
|
+
import { buildCountSql, buildEstimateSql, buildFindSql, resolvePagination, scopeSuffix } from './builder.js';
|
|
7
7
|
import { deleteDetailsChildren, insertDetails } from './details.js';
|
|
8
8
|
import { insertLinks, replaceLinks } from './link.js';
|
|
9
|
-
import { runWorkflowTransition, getWorkflowStatus, acquireWorkflowLock, releaseWorkflowLock, getWorkflowHistory, getWorkflowTodos, overrideWorkflow, } from './workflow.js';
|
|
9
|
+
import { WORKFLOW_ACTIONS, runWorkflowTransition, getWorkflowStatus, acquireWorkflowLock, releaseWorkflowLock, getWorkflowHistory, getWorkflowTodos, overrideWorkflow, } from './workflow.js';
|
|
10
10
|
import { computeFormulas } from './formula.js';
|
|
11
11
|
import { generateSeqNo } from './seqno.js';
|
|
12
12
|
import { subjectOf, scopeOf } from './types.js';
|
|
13
13
|
import { validateRecord } from './validate.js';
|
|
14
|
-
import { FILTER_OPS, SORT_DIRS, WRITE_MODES } from './values.js';
|
|
14
|
+
import { FILTER_OPS, SORT_DIRS, TOTAL_MODES, WRITE_MODES } from './values.js';
|
|
15
15
|
import { decodeCursor, encodeCursor } from './cursor.js';
|
|
16
16
|
import { QUERY_BUDGET_DEFAULTS, assertQueryBudget } from './query-budget.js';
|
|
17
|
-
import { EXECUTION_OUTCOMES
|
|
17
|
+
import { EXECUTION_OUTCOMES } from '../../core/index.js';
|
|
18
|
+
import { runPipeline } from '../execution/index.js';
|
|
18
19
|
import { upsertRecordMeta, deleteRecordMeta } from '../record-meta/index.js';
|
|
20
|
+
import { SYSTEM_TABLES } from '../../core/storage/system-tables.js';
|
|
21
|
+
import { asRlsRole } from './rls-session.js';
|
|
19
22
|
const q = (id) => `"${id}"`;
|
|
20
23
|
/** cross-object formula authorization derived from the request context */
|
|
21
24
|
function formulaAuth(ctx) {
|
|
@@ -168,6 +171,11 @@ function pkValuesOf(def, id, ctx) {
|
|
|
168
171
|
}
|
|
169
172
|
return values;
|
|
170
173
|
}
|
|
174
|
+
/** reject a malformed external id up front (keyed objects only; key-less objects 404 later) */
|
|
175
|
+
function validateId(def, id, ctx) {
|
|
176
|
+
if (primaryNames(def).length > 0)
|
|
177
|
+
pkValuesOf(def, id, ctx);
|
|
178
|
+
}
|
|
171
179
|
/** primary-key filter object for `find` (declared names zipped with values) */
|
|
172
180
|
function pkFilter(def, values) {
|
|
173
181
|
const filter = {};
|
|
@@ -264,6 +272,10 @@ export class DefaultObjectDataAccess {
|
|
|
264
272
|
schemaRevision;
|
|
265
273
|
/** optional metrics sink (query-budget rejections); absent = zero overhead */
|
|
266
274
|
metrics;
|
|
275
|
+
/** object-path RLS role (`rls.enforce`); user-principal customer-table reads/updates/deletes run under it */
|
|
276
|
+
rlsRole;
|
|
277
|
+
/** default `total` computation for `find` (per-call `opts.total` overrides) */
|
|
278
|
+
totalMode;
|
|
267
279
|
constructor(options = {}) {
|
|
268
280
|
this.audit = options.audit;
|
|
269
281
|
this.script = options.script ?? NOOP_SCRIPT_DISPATCHER;
|
|
@@ -276,6 +288,43 @@ export class DefaultObjectDataAccess {
|
|
|
276
288
|
this.evidence = options.evidence;
|
|
277
289
|
this.schemaRevision = options.schemaRevision;
|
|
278
290
|
this.metrics = options.metrics;
|
|
291
|
+
if (options.rls !== undefined && !isSafeRlsRole(options.rls.role)) {
|
|
292
|
+
throw new Error(`invalid RLS role name "${options.rls.role}"`);
|
|
293
|
+
}
|
|
294
|
+
this.rlsRole = options.rls?.role;
|
|
295
|
+
this.totalMode = options.total ?? TOTAL_MODES.EXACT;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Run customer-table statements under the object RLS role for a user
|
|
299
|
+
* principal (a pool request gets its own short transaction); system
|
|
300
|
+
* principals and enforcement-off run as the owner.
|
|
301
|
+
*/
|
|
302
|
+
async readScoped(ctx, fn) {
|
|
303
|
+
const subject = this.rlsRole === undefined ? undefined : subjectOf(ctx);
|
|
304
|
+
if (subject === undefined)
|
|
305
|
+
return fn(ctx.client ?? ctx.pool);
|
|
306
|
+
const role = this.rlsRole;
|
|
307
|
+
if (ctx.client !== undefined)
|
|
308
|
+
return asRlsRole(ctx.client, role, subject, () => fn(ctx.client));
|
|
309
|
+
const client = await ctx.pool.connect();
|
|
310
|
+
try {
|
|
311
|
+
await client.query('BEGIN');
|
|
312
|
+
const result = await asRlsRole(client, role, subject, () => fn(client));
|
|
313
|
+
await client.query('COMMIT');
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
await client.query('ROLLBACK').catch(() => { });
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
client.release();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** {@link readScoped} for a client already inside the write transaction */
|
|
325
|
+
underRls(client, ctx, fn) {
|
|
326
|
+
const subject = this.rlsRole === undefined ? undefined : subjectOf(ctx);
|
|
327
|
+
return subject === undefined ? fn() : asRlsRole(client, this.rlsRole, subject, fn);
|
|
279
328
|
}
|
|
280
329
|
/**
|
|
281
330
|
* Run the read hook (`onLoad`) over a fetched batch. The hook receives the
|
|
@@ -353,9 +402,9 @@ export class DefaultObjectDataAccess {
|
|
|
353
402
|
}
|
|
354
403
|
async find(objectName, opts, ctx) {
|
|
355
404
|
const def = requireDef(ctx, objectName);
|
|
356
|
-
const q = ctx.client ?? ctx.pool;
|
|
357
405
|
const pks = primaryFieldsOf(def);
|
|
358
406
|
const budget = ctx.budget ?? this.budget;
|
|
407
|
+
const totalMode = opts.total ?? this.totalMode;
|
|
359
408
|
try {
|
|
360
409
|
assertQueryBudget(opts, budget, ctx.locale);
|
|
361
410
|
}
|
|
@@ -369,6 +418,21 @@ export class DefaultObjectDataAccess {
|
|
|
369
418
|
throw error;
|
|
370
419
|
}
|
|
371
420
|
const { limit } = resolvePagination(opts, budget.maxRows);
|
|
421
|
+
const noneTotal = totalMode === TOTAL_MODES.NONE;
|
|
422
|
+
// `total` per the effective mode: exact COUNT(*), a reltuples estimate, or
|
|
423
|
+
// nothing (none). Counts run on the same (RLS-scoped) connection as the page.
|
|
424
|
+
const countTotal = async (db, buildCtx) => {
|
|
425
|
+
if (noneTotal)
|
|
426
|
+
return undefined;
|
|
427
|
+
if (totalMode === TOTAL_MODES.ESTIMATE) {
|
|
428
|
+
const { sql, params } = buildEstimateSql(def);
|
|
429
|
+
const { rows } = await runTableQuery(db, objectName, sql, params, ctx.locale);
|
|
430
|
+
return rows[0]?.total;
|
|
431
|
+
}
|
|
432
|
+
const { sql, params } = buildCountSql(def, opts, buildCtx, ctx.rowScope);
|
|
433
|
+
const { rows } = await runTableQuery(db, objectName, sql, params, ctx.locale);
|
|
434
|
+
return rows[0].total;
|
|
435
|
+
};
|
|
372
436
|
// keyset cursor (single primary key): `pk > cursor` ordered by pk. A
|
|
373
437
|
// caller-provided sort is ignored in cursor mode (documented) so pages
|
|
374
438
|
// traverse one deterministic sequence.
|
|
@@ -385,17 +449,18 @@ export class DefaultObjectDataAccess {
|
|
|
385
449
|
const meta = needsSideTable(pageOpts) ? { table: recordMetaTableName(def.name) } : undefined;
|
|
386
450
|
const buildCtx = { ...bctx(ctx, objectName), ...(meta === undefined ? {} : { meta }) };
|
|
387
451
|
const { sql, params } = buildFindSql(def, pageOpts, buildCtx, ctx.rowScope, pageOpts.exclude, undefined, budget.maxRows + 1);
|
|
388
|
-
const { rows } = await
|
|
452
|
+
const [{ rows }, total] = await this.readScoped(ctx, async (db) => [
|
|
453
|
+
await runTableQuery(db, objectName, sql, params, ctx.locale),
|
|
454
|
+
await countTotal(db, buildCtx),
|
|
455
|
+
]);
|
|
389
456
|
const hasMore = rows.length > limit;
|
|
390
457
|
const pageRows = hasMore ? rows.slice(0, limit) : rows;
|
|
391
458
|
const last = pageRows[pageRows.length - 1];
|
|
392
459
|
const nextCursor = hasMore && last !== undefined ? encodeCursor([last[pkName]]) : undefined;
|
|
393
|
-
const { sql: countSql, params: countParams } = buildCountSql(def, opts, buildCtx, ctx.rowScope);
|
|
394
|
-
const { rows: countRows } = await runTableQuery(q, objectName, countSql, countParams, ctx.locale);
|
|
395
460
|
const loaded = await this.runOnLoad(objectName, pageRows, ctx);
|
|
396
461
|
return {
|
|
397
462
|
rows: loaded,
|
|
398
|
-
total:
|
|
463
|
+
...(total === undefined ? {} : { total }),
|
|
399
464
|
hasMore,
|
|
400
465
|
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
401
466
|
};
|
|
@@ -404,13 +469,19 @@ export class DefaultObjectDataAccess {
|
|
|
404
469
|
// LEFT JOIN it and select/filter/sort them in SQL (never customer columns)
|
|
405
470
|
const meta = needsSideTable(opts) ? { table: recordMetaTableName(def.name) } : undefined;
|
|
406
471
|
const buildCtx = { ...bctx(ctx, objectName), ...(meta === undefined ? {} : { meta }) };
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const {
|
|
410
|
-
const {
|
|
411
|
-
const total =
|
|
412
|
-
|
|
413
|
-
|
|
472
|
+
// with `total: 'none'` there is no count query to derive `hasMore`, so fetch
|
|
473
|
+
// one extra row (like cursor mode) and slice it off
|
|
474
|
+
const findOpts = noneTotal ? { ...opts, limit: limit + 1 } : opts;
|
|
475
|
+
const { sql, params } = buildFindSql(def, findOpts, buildCtx, ctx.rowScope, findOpts.exclude, undefined, noneTotal ? budget.maxRows + 1 : budget.maxRows);
|
|
476
|
+
const [{ rows }, total] = await this.readScoped(ctx, async (db) => [
|
|
477
|
+
await runTableQuery(db, objectName, sql, params, ctx.locale),
|
|
478
|
+
await countTotal(db, buildCtx),
|
|
479
|
+
]);
|
|
480
|
+
const offset = opts.offset ?? 0;
|
|
481
|
+
const hasMore = noneTotal ? rows.length > limit : offset + rows.length < (total ?? 0);
|
|
482
|
+
const pageRows = noneTotal && hasMore ? rows.slice(0, limit) : rows;
|
|
483
|
+
const loaded = await this.runOnLoad(objectName, pageRows, ctx);
|
|
484
|
+
return { rows: loaded, ...(total === undefined ? {} : { total }), hasMore };
|
|
414
485
|
}
|
|
415
486
|
async findOne(objectName, id, ctx) {
|
|
416
487
|
const def = requireDef(ctx, objectName);
|
|
@@ -425,22 +496,25 @@ export class DefaultObjectDataAccess {
|
|
|
425
496
|
const result = await this.find(objectName, { filter: pkFilter(def, pkValuesOf(def, id, ctx)), limit: 1, fields: [...cols, RECORD_META_ID_FIELD] }, ctx);
|
|
426
497
|
return result.rows[0] ?? null;
|
|
427
498
|
}
|
|
428
|
-
/**
|
|
429
|
-
|
|
430
|
-
* Runs only when policies are configured (`policies=[]` → zero overhead) and
|
|
431
|
-
* is protocol-agnostic (action `object.<object>.<op>`), so REST / MCP /
|
|
432
|
-
* GraphQL / script writes are all covered from this single choke point.
|
|
433
|
-
* Deny throws; a `requiresApproval` policy throws with the approval key.
|
|
434
|
-
*/
|
|
435
|
-
async guardWrite(op, objectName, ctx, args) {
|
|
436
|
-
if (this.policies.length === 0)
|
|
437
|
-
return;
|
|
438
|
-
const action = `object.${objectName}.${op}`;
|
|
499
|
+
/** pipeline context (actor/subject/correlation) for a governed write */
|
|
500
|
+
pipelineCtx(ctx) {
|
|
439
501
|
const subject = subjectOf(ctx);
|
|
440
502
|
const actorId = principalActorId(ctx.principal);
|
|
441
|
-
const
|
|
503
|
+
const schemaRevision = ctx.schemaRevision ?? this.schemaRevision;
|
|
504
|
+
return {
|
|
442
505
|
actor: { key: actorId, label: subject?.id ?? 'system', onBehalfOf: subject?.id ?? actorId },
|
|
443
506
|
subject: subject ?? { id: 'system', roles: [] },
|
|
507
|
+
...(ctx.requestId === undefined ? {} : { requestId: ctx.requestId }),
|
|
508
|
+
...(ctx.traceId === undefined ? {} : { traceId: ctx.traceId }),
|
|
509
|
+
...(schemaRevision === undefined ? {} : { schemaRevision }),
|
|
510
|
+
...(ctx.locale === undefined ? {} : { locale: ctx.locale }),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/** guardrail + approval decision for `action` (shared by writes and transitions) */
|
|
514
|
+
async decide(action, args, ctx, pctx, requireApproval) {
|
|
515
|
+
const guardrailCtx = {
|
|
516
|
+
actor: pctx.actor,
|
|
517
|
+
subject: pctx.subject,
|
|
444
518
|
action,
|
|
445
519
|
args,
|
|
446
520
|
dataAccess: toolDataAccessOver(this, {
|
|
@@ -451,55 +525,67 @@ export class DefaultObjectDataAccess {
|
|
|
451
525
|
...(ctx.client === undefined ? {} : { client: ctx.client }),
|
|
452
526
|
}),
|
|
453
527
|
};
|
|
454
|
-
const gate = await evaluateTransition(this.policies, this.approvals, guardrailCtx,
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
if (
|
|
494
|
-
|
|
528
|
+
const gate = await evaluateTransition(this.policies, this.approvals, guardrailCtx, requireApproval);
|
|
529
|
+
if (gate.kind === 'deny')
|
|
530
|
+
return { kind: 'deny', reason: gate.reason ?? '', errorCode: gate.code };
|
|
531
|
+
return gate;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Run a write (create/update/delete/transition) through the execution
|
|
535
|
+
* pipeline: guardrail/approval gate (when policies or `requireApproval` apply)
|
|
536
|
+
* → execute → one evidence row (when evidence is enabled). Protocol-agnostic
|
|
537
|
+
* (action `object.<object>.<op>` / `workflow.transition.<object>.<action>`),
|
|
538
|
+
* so REST / MCP / GraphQL / script writes share this single choke point.
|
|
539
|
+
* Without policies, approval or evidence the write runs directly.
|
|
540
|
+
*/
|
|
541
|
+
async governed(plan, ctx, opts, execute) {
|
|
542
|
+
const gated = this.policies.length > 0 || opts.requireApproval !== undefined;
|
|
543
|
+
if (!gated && this.evidence === undefined)
|
|
544
|
+
return execute();
|
|
545
|
+
const reject = async (error) => {
|
|
546
|
+
await opts.onReject?.(error);
|
|
547
|
+
throw error;
|
|
548
|
+
};
|
|
549
|
+
if (opts.preflight !== undefined)
|
|
550
|
+
await opts.preflight().catch(reject);
|
|
551
|
+
const pctx = this.pipelineCtx(ctx);
|
|
552
|
+
const result = await runPipeline(plan, pctx, {
|
|
553
|
+
...(gated
|
|
554
|
+
? {
|
|
555
|
+
gate: async () => this.decide(plan.action, plan.args, ctx, pctx, (await opts.requireApproval?.()) ?? false),
|
|
556
|
+
}
|
|
557
|
+
: {}),
|
|
558
|
+
execute,
|
|
559
|
+
...(this.evidence === undefined ? {} : { evidence: this.evidence }),
|
|
560
|
+
});
|
|
561
|
+
const params = { object: plan.objectName ?? '', action: opts.actionParam ?? plan.action };
|
|
562
|
+
if (result.outcome === EXECUTION_OUTCOMES.OK)
|
|
563
|
+
return result.result;
|
|
564
|
+
if (result.outcome === EXECUTION_OUTCOMES.PENDING) {
|
|
565
|
+
return reject(new SchemaError(opts.pendingCode, { ...params, approvalKey: result.approvalKey ?? '' }, ctx.locale));
|
|
566
|
+
}
|
|
567
|
+
if (result.outcome === EXECUTION_OUTCOMES.DENY) {
|
|
568
|
+
return reject(new SchemaError((result.errorCode ?? 'mcp.policy.denied'), { ...params, reason: result.reason ?? '' }, ctx.locale));
|
|
495
569
|
}
|
|
570
|
+
throw result.error;
|
|
496
571
|
}
|
|
497
572
|
async create(objectName, data, ctx) {
|
|
573
|
+
requireDef(ctx, objectName);
|
|
574
|
+
return this.governed({ action: `object.${objectName}.create`, objectName, args: data }, ctx, { pendingCode: 'object.action.pending' }, () => this.doCreate(objectName, data, ctx));
|
|
575
|
+
}
|
|
576
|
+
async update(objectName, id, changes, ctx) {
|
|
577
|
+
validateId(requireDef(ctx, objectName), id, ctx);
|
|
578
|
+
return this.governed({ action: `object.${objectName}.update`, objectName, objectId: id, args: changes }, ctx, { pendingCode: 'object.action.pending' }, () => this.doUpdate(objectName, id, changes, ctx));
|
|
579
|
+
}
|
|
580
|
+
async delete(objectName, id, ctx) {
|
|
581
|
+
validateId(requireDef(ctx, objectName), id, ctx);
|
|
582
|
+
return this.governed({ action: `object.${objectName}.delete`, objectName, objectId: id, args: {} }, ctx, { pendingCode: 'object.action.pending' }, () => this.doDelete(objectName, id, ctx));
|
|
583
|
+
}
|
|
584
|
+
async doCreate(objectName, data, ctx) {
|
|
498
585
|
const def = requireDef(ctx, objectName);
|
|
499
586
|
if (primaryNames(def).length === 0) {
|
|
500
587
|
throw new SchemaError('data.recordNotFound', { object: objectName }, ctx.locale);
|
|
501
588
|
}
|
|
502
|
-
await this.guardWrite('create', objectName, ctx, data);
|
|
503
589
|
let client;
|
|
504
590
|
let owned = false;
|
|
505
591
|
if (ctx.client !== undefined) {
|
|
@@ -614,13 +700,12 @@ export class DefaultObjectDataAccess {
|
|
|
614
700
|
client.release();
|
|
615
701
|
}
|
|
616
702
|
}
|
|
617
|
-
async
|
|
703
|
+
async doUpdate(objectName, id, changes, ctx) {
|
|
618
704
|
const def = requireDef(ctx, objectName);
|
|
619
705
|
if (primaryNames(def).length === 0) {
|
|
620
706
|
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
621
707
|
}
|
|
622
708
|
const pkValues = pkValuesOf(def, id, ctx);
|
|
623
|
-
await this.guardWrite('update', objectName, ctx, changes);
|
|
624
709
|
let client;
|
|
625
710
|
let owned = false;
|
|
626
711
|
if (ctx.client !== undefined) {
|
|
@@ -636,7 +721,7 @@ export class DefaultObjectDataAccess {
|
|
|
636
721
|
await client.query('BEGIN');
|
|
637
722
|
await validateRecord(def, changes, WRITE_MODES.UPDATE, { pool: client, registry: ctx.registry, locale: ctx.locale });
|
|
638
723
|
const { sql, params } = buildFindSql(def, { filter: pkFilter(def, pkValues), limit: 1 }, bctx(ctx, objectName), ctx.rowScope, undefined, [recordKeySelect(def)]);
|
|
639
|
-
const { rows } = await runTableQuery(client, objectName, sql, params, ctx.locale);
|
|
724
|
+
const { rows } = await this.underRls(client, ctx, () => runTableQuery(client, objectName, sql, params, ctx.locale));
|
|
640
725
|
const raw = rows[0];
|
|
641
726
|
if (raw === undefined) {
|
|
642
727
|
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
@@ -692,7 +777,9 @@ export class DefaultObjectDataAccess {
|
|
|
692
777
|
const setSql = cols.map((c, i) => `${q(c)} = $${i + 1}`).join(', ');
|
|
693
778
|
const scope = ctx.rowScope !== undefined ? scopeSuffix(ctx.rowScope, cols.length + pkValues.length) : undefined;
|
|
694
779
|
const values = cols.map((c) => dbJsonValue(def.fields.find((f) => f.name === c), record[c] ?? null));
|
|
695
|
-
await runTableQuery(client, objectName, `UPDATE ${q(def.name)} SET ${setSql} WHERE ${pkWhereSql(def, cols.length + 1)}${scope !== undefined ? ` AND (${scope.sql})` : ''}`, [...values, ...pkValues, ...(scope?.params ?? [])], ctx.locale);
|
|
780
|
+
const res = await this.underRls(client, ctx, () => runTableQuery(client, objectName, `UPDATE ${q(def.name)} SET ${setSql} WHERE ${pkWhereSql(def, cols.length + 1)}${scope !== undefined ? ` AND (${scope.sql})` : ''}`, [...values, ...pkValues, ...(scope?.params ?? [])], ctx.locale));
|
|
781
|
+
if (res.rowCount === 0)
|
|
782
|
+
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
696
783
|
}
|
|
697
784
|
await this.writeRecordMeta(client, def, metaKey, ctx, now, false);
|
|
698
785
|
await replaceLinks(client, def, pkValues, payload, ctx.registry, ctx.locale);
|
|
@@ -731,13 +818,51 @@ export class DefaultObjectDataAccess {
|
|
|
731
818
|
*/
|
|
732
819
|
async transition(objectName, id, action, ctx, payload) {
|
|
733
820
|
const def = requireDef(ctx, objectName);
|
|
734
|
-
|
|
821
|
+
const workflow = def.workflow;
|
|
822
|
+
if (workflow === undefined) {
|
|
735
823
|
throw new SchemaError('workflow.transition.unknown', { object: objectName, action }, ctx.locale);
|
|
736
824
|
}
|
|
737
825
|
if (primaryNames(def).length === 0) {
|
|
738
826
|
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
739
827
|
}
|
|
740
828
|
const pkValues = pkValuesOf(def, id, ctx);
|
|
829
|
+
// a policy self-filters on `workflow.transition.<object>.<action>`; a node may
|
|
830
|
+
// also declare `requiresApproval` (deny → error; requireApproval → pending)
|
|
831
|
+
const approvalNodes = workflow.nodes.some((n) => n.requiresApproval === true);
|
|
832
|
+
return this.governed({ action: `workflow.transition.${def.name}.${action}`, objectName, objectId: id, args: { object: def.name, id, action } }, ctx, {
|
|
833
|
+
pendingCode: 'workflow.transition.pending',
|
|
834
|
+
actionParam: action,
|
|
835
|
+
preflight: async () => {
|
|
836
|
+
const [sql, params] = this.transitionTargetSql(def, pkValues, action, ctx);
|
|
837
|
+
const { rows } = action === WORKFLOW_ACTIONS.SUBMIT
|
|
838
|
+
? await this.readScoped(ctx, (db) => runTableQuery(db, objectName, sql, params, ctx.locale))
|
|
839
|
+
: await runTableQuery(ctx.client ?? ctx.pool, objectName, sql, params, ctx.locale);
|
|
840
|
+
if (rows[0] === undefined)
|
|
841
|
+
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
842
|
+
},
|
|
843
|
+
onReject: (error) => auditWrite(this.audit, ctx, DATA_ACTIONS.TRANSITION, objectName, id, { action }, error),
|
|
844
|
+
...(approvalNodes
|
|
845
|
+
? {
|
|
846
|
+
requireApproval: async () => {
|
|
847
|
+
const status = await getWorkflowStatus(ctx.client ?? ctx.pool, def, id, subjectOf(ctx));
|
|
848
|
+
return workflow.nodes.find((n) => n.id === status.node)?.requiresApproval === true;
|
|
849
|
+
},
|
|
850
|
+
}
|
|
851
|
+
: {}),
|
|
852
|
+
}, () => this.doTransition(def, id, pkValues, action, ctx, payload));
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Existence check for a transition target. Submit is an owner action → the
|
|
856
|
+
* caller's manage scope; the other actions are authorized by workitem
|
|
857
|
+
* assignment / originator, not by row scope.
|
|
858
|
+
*/
|
|
859
|
+
transitionTargetSql(def, pkValues, action, ctx) {
|
|
860
|
+
const { sql, params } = buildFindSql(def, { filter: pkFilter(def, pkValues), limit: 1 }, bctx(ctx, def.name), action === WORKFLOW_ACTIONS.SUBMIT ? ctx.rowScope : undefined, undefined, [recordKeySelect(def)]);
|
|
861
|
+
return [sql, params];
|
|
862
|
+
}
|
|
863
|
+
async doTransition(def, id, pkValues, action, ctx, payload) {
|
|
864
|
+
const objectName = def.name;
|
|
865
|
+
const workflow = def.workflow;
|
|
741
866
|
let client;
|
|
742
867
|
let owned = false;
|
|
743
868
|
if (ctx.client !== undefined) {
|
|
@@ -751,41 +876,12 @@ export class DefaultObjectDataAccess {
|
|
|
751
876
|
if (owned)
|
|
752
877
|
await client.query('BEGIN');
|
|
753
878
|
// the customer record must exist (transitions never create records)
|
|
754
|
-
const
|
|
755
|
-
const
|
|
879
|
+
const [sql, params] = this.transitionTargetSql(def, pkValues, action, ctx);
|
|
880
|
+
const lookup = () => runTableQuery(client, objectName, sql, params, ctx.locale);
|
|
881
|
+
const found = action === WORKFLOW_ACTIONS.SUBMIT ? await this.underRls(client, ctx, lookup) : await lookup();
|
|
756
882
|
if (found.rows[0] === undefined) {
|
|
757
883
|
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
758
884
|
}
|
|
759
|
-
// guardrail policy + approval gate (shared policy set with the open contract):
|
|
760
|
-
// a policy self-filters on `workflow.transition.<object>.<action>`; a node may
|
|
761
|
-
// also declare `requiresApproval`. deny → error; requireApproval → pending.
|
|
762
|
-
const currentNode = await getWorkflowStatus(client, def, id, subjectOf(ctx));
|
|
763
|
-
const nodeDef = currentNode.node === undefined ? undefined : def.workflow.nodes.find((n) => n.id === currentNode.node);
|
|
764
|
-
const requireApproval = nodeDef?.requiresApproval === true;
|
|
765
|
-
if (this.policies.length > 0 || requireApproval) {
|
|
766
|
-
const subject = subjectOf(ctx);
|
|
767
|
-
const actorId = subject?.id ?? principalActorId(ctx.principal);
|
|
768
|
-
const guardrailCtx = {
|
|
769
|
-
actor: { key: actorId, label: actorId, onBehalfOf: actorId },
|
|
770
|
-
subject: subject ?? { id: 'system', roles: [] },
|
|
771
|
-
action: `workflow.transition.${def.name}.${action}`,
|
|
772
|
-
args: { object: def.name, id, action },
|
|
773
|
-
dataAccess: toolDataAccessOver(this, {
|
|
774
|
-
pool: ctx.pool,
|
|
775
|
-
registry: ctx.registry,
|
|
776
|
-
principal: ctx.principal,
|
|
777
|
-
...(ctx.locale === undefined ? {} : { locale: ctx.locale }),
|
|
778
|
-
...(ctx.client === undefined ? {} : { client: ctx.client }),
|
|
779
|
-
}),
|
|
780
|
-
};
|
|
781
|
-
const gate = await evaluateTransition(this.policies, this.approvals, guardrailCtx, requireApproval);
|
|
782
|
-
if (gate.kind === 'deny') {
|
|
783
|
-
throw new SchemaError(gate.code, { object: objectName, action, reason: gate.reason ?? '' }, ctx.locale);
|
|
784
|
-
}
|
|
785
|
-
if (gate.kind === 'pending') {
|
|
786
|
-
throw new SchemaError('workflow.transition.pending', { object: objectName, action, approvalKey: gate.approvalKey }, ctx.locale);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
885
|
const outcome = await runWorkflowTransition(client, def, id, action, subjectOf(ctx), payload, ctx.locale);
|
|
790
886
|
const transitionChanges = {
|
|
791
887
|
action,
|
|
@@ -803,7 +899,7 @@ export class DefaultObjectDataAccess {
|
|
|
803
899
|
await auditWrite(this.audit, ctx, DATA_ACTIONS.TRANSITION, objectName, id, transitionChanges);
|
|
804
900
|
}
|
|
805
901
|
this.events?.publishRecordChange('updated', objectName, id);
|
|
806
|
-
this.events?.publishRecordTransitioned(objectName, id, outcome.fromNodeId ?? '', outcome.toNodeId ?? '', action,
|
|
902
|
+
this.events?.publishRecordTransitioned(objectName, id, outcome.fromNodeId ?? '', outcome.toNodeId ?? '', action, workflow.version, def.workflowHash);
|
|
807
903
|
if (outcome.nodeId !== undefined) {
|
|
808
904
|
await this.workflowTimers?.sync(objectName, id, outcome.nodeId).catch(() => { });
|
|
809
905
|
}
|
|
@@ -841,8 +937,34 @@ export class DefaultObjectDataAccess {
|
|
|
841
937
|
if (def.workflow === undefined) {
|
|
842
938
|
throw new SchemaError('workflow.transition.unknown', { object: objectName, action: 'status' }, ctx.locale);
|
|
843
939
|
}
|
|
940
|
+
await this.assertWorkflowVisible(def, id, ctx);
|
|
844
941
|
return getWorkflowStatus(ctx.client ?? ctx.pool, def, id, subjectOf(ctx));
|
|
845
942
|
}
|
|
943
|
+
/**
|
|
944
|
+
* A record's workflow is visible when the record is inside the caller's read
|
|
945
|
+
* scope, or the caller participates (originator / any workitem) — an assigned
|
|
946
|
+
* approver may sit outside the record's row scope.
|
|
947
|
+
*/
|
|
948
|
+
async assertWorkflowVisible(def, id, ctx) {
|
|
949
|
+
if (ctx.rowScope === undefined)
|
|
950
|
+
return;
|
|
951
|
+
const db = ctx.client ?? ctx.pool;
|
|
952
|
+
const pkValues = pkValuesOf(def, id, ctx);
|
|
953
|
+
const { sql, params } = buildFindSql(def, { filter: pkFilter(def, pkValues), limit: 1 }, bctx(ctx, def.name), ctx.rowScope);
|
|
954
|
+
const { rows } = await this.readScoped(ctx, (scoped) => runTableQuery(scoped, def.name, sql, params, ctx.locale));
|
|
955
|
+
if (rows.length > 0)
|
|
956
|
+
return;
|
|
957
|
+
const subject = subjectOf(ctx);
|
|
958
|
+
if (subject !== undefined) {
|
|
959
|
+
const res = await db.query(`SELECT 1 FROM ${q(SYSTEM_TABLES.WORKFLOW_INSTANCES)} WHERE object = $1 AND record_key = $2 AND originator = $3
|
|
960
|
+
UNION ALL
|
|
961
|
+
SELECT 1 FROM ${q(SYSTEM_TABLES.WORKFLOW_WORKITEMS)} WHERE object = $1 AND record_key = $2 AND participant = $3
|
|
962
|
+
LIMIT 1`, [def.name, id, subject.id]);
|
|
963
|
+
if (res.rows.length > 0)
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
throw new SchemaError('data.recordNotFound', { object: def.name, id }, ctx.locale);
|
|
967
|
+
}
|
|
846
968
|
/** Acquire/renew the caller's presence lock on the current step (TTL lease). */
|
|
847
969
|
async acquireWorkflowLock(objectName, id, ctx) {
|
|
848
970
|
const def = requireDef(ctx, objectName);
|
|
@@ -859,6 +981,7 @@ export class DefaultObjectDataAccess {
|
|
|
859
981
|
if (def.workflow === undefined) {
|
|
860
982
|
throw new SchemaError('workflow.transition.unknown', { object: objectName, action: 'history' }, ctx.locale);
|
|
861
983
|
}
|
|
984
|
+
await this.assertWorkflowVisible(def, id, ctx);
|
|
862
985
|
return getWorkflowHistory(ctx.client ?? ctx.pool, def, id);
|
|
863
986
|
}
|
|
864
987
|
/** The subject's pending workitems across all objects. */
|
|
@@ -914,13 +1037,12 @@ export class DefaultObjectDataAccess {
|
|
|
914
1037
|
? { createdBy: actor, modifiedBy: actor, createdTime: now, modifiedTime: now }
|
|
915
1038
|
: { modifiedBy: actor, modifiedTime: now });
|
|
916
1039
|
}
|
|
917
|
-
async
|
|
1040
|
+
async doDelete(objectName, id, ctx) {
|
|
918
1041
|
const def = requireDef(ctx, objectName);
|
|
919
1042
|
if (primaryNames(def).length === 0) {
|
|
920
1043
|
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
921
1044
|
}
|
|
922
1045
|
const pkValues = pkValuesOf(def, id, ctx);
|
|
923
|
-
await this.guardWrite('delete', objectName, ctx, {});
|
|
924
1046
|
let client;
|
|
925
1047
|
let owned = false;
|
|
926
1048
|
if (ctx.client !== undefined) {
|
|
@@ -935,7 +1057,7 @@ export class DefaultObjectDataAccess {
|
|
|
935
1057
|
if (owned)
|
|
936
1058
|
await client.query('BEGIN');
|
|
937
1059
|
const { sql, params } = buildFindSql(def, { filter: pkFilter(def, pkValues), limit: 1 }, bctx(ctx, objectName), ctx.rowScope, undefined, [recordKeySelect(def)]);
|
|
938
|
-
const { rows } = await runTableQuery(client, objectName, sql, params, ctx.locale);
|
|
1060
|
+
const { rows } = await this.underRls(client, ctx, () => runTableQuery(client, objectName, sql, params, ctx.locale));
|
|
939
1061
|
const raw = rows[0];
|
|
940
1062
|
if (raw === undefined) {
|
|
941
1063
|
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
@@ -946,7 +1068,7 @@ export class DefaultObjectDataAccess {
|
|
|
946
1068
|
await this.runBeforeHook(SCRIPT_HOOKS.BEFORE_DELETE, objectName, existing, {}, ctx);
|
|
947
1069
|
await deleteDetailsChildren(client, def, id, ctx.registry);
|
|
948
1070
|
const scope = ctx.rowScope !== undefined ? scopeSuffix(ctx.rowScope, pkValues.length) : undefined;
|
|
949
|
-
const res = await runTableQuery(client, objectName, `DELETE FROM ${q(def.name)} WHERE ${pkWhereSql(def, 1)}${scope !== undefined ? ` AND (${scope.sql})` : ''}`, [...pkValues, ...(scope?.params ?? [])], ctx.locale);
|
|
1071
|
+
const res = await this.underRls(client, ctx, () => runTableQuery(client, objectName, `DELETE FROM ${q(def.name)} WHERE ${pkWhereSql(def, 1)}${scope !== undefined ? ` AND (${scope.sql})` : ''}`, [...pkValues, ...(scope?.params ?? [])], ctx.locale));
|
|
950
1072
|
if (res.rowCount === 0)
|
|
951
1073
|
throw new SchemaError('data.recordNotFound', { object: objectName, id }, ctx.locale);
|
|
952
1074
|
await deleteRecordMeta(client, def.name, metaKey);
|