@objectstack/objectql 6.7.1 → 6.8.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/dist/index.mjs CHANGED
@@ -622,9 +622,26 @@ var SchemaRegistry = class {
622
622
  // src/sys-metadata-repository.ts
623
623
  import { hashSpec, ConflictError } from "@objectstack/metadata-core";
624
624
  import { DEFAULT_METADATA_TYPE_REGISTRY } from "@objectstack/spec/kernel";
625
+ import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from "@objectstack/spec/shared";
625
626
  var OVERLAY_ALLOWED_TYPES = new Set(
626
627
  DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
627
628
  );
629
+ var _envWritableMetadataTypes = null;
630
+ function envWritableMetadataTypes() {
631
+ if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
632
+ const raw = typeof process !== "undefined" && process?.env?.OBJECTSTACK_METADATA_WRITABLE || "";
633
+ const set = /* @__PURE__ */ new Set();
634
+ for (const tok of raw.split(",")) {
635
+ const t = tok.trim();
636
+ if (!t) continue;
637
+ const singular = PLURAL_TO_SINGULAR[t] ?? t;
638
+ set.add(singular);
639
+ const plural = SINGULAR_TO_PLURAL[singular];
640
+ if (plural) set.add(plural);
641
+ }
642
+ _envWritableMetadataTypes = set;
643
+ return set;
644
+ }
628
645
  var SysMetadataRepository = class {
629
646
  constructor(opts) {
630
647
  /**
@@ -1017,14 +1034,21 @@ var SysMetadataRepository = class {
1017
1034
  if (this.closed) throw new Error("SysMetadataRepository is closed");
1018
1035
  }
1019
1036
  assertAllowed(type) {
1020
- if (!OVERLAY_ALLOWED_TYPES.has(type)) {
1021
- const err = new Error(
1022
- `[not_overridable] '${type}' is not allowOrgOverride in the registry. Allowed: ${Array.from(OVERLAY_ALLOWED_TYPES).join(", ")}.`
1023
- );
1024
- err.code = "not_overridable";
1025
- err.status = 403;
1026
- throw err;
1027
- }
1037
+ const singular = PLURAL_TO_SINGULAR[type] ?? type;
1038
+ const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
1039
+ if (allowedByRegistry) return;
1040
+ const env = envWritableMetadataTypes();
1041
+ if (env.has(singular) || env.has(type)) return;
1042
+ const allowed = [
1043
+ ...OVERLAY_ALLOWED_TYPES,
1044
+ ...envWritableMetadataTypes()
1045
+ ];
1046
+ const err = new Error(
1047
+ `[not_overridable] '${type}' is not allowOrgOverride in the registry. Allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
1048
+ );
1049
+ err.code = "not_overridable";
1050
+ err.status = 403;
1051
+ throw err;
1028
1052
  }
1029
1053
  whereFor(ref) {
1030
1054
  return {
@@ -1127,13 +1151,171 @@ var SysMetadataRepository = class {
1127
1151
 
1128
1152
  // src/protocol.ts
1129
1153
  import { ConflictError as ConflictError2 } from "@objectstack/metadata-core";
1130
- import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
1131
- import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from "@objectstack/spec/shared";
1132
- import { ListViewSchema, FormViewSchema, DashboardSchema } from "@objectstack/spec/ui";
1154
+ import { parseFilterAST, isFilterAST, objectForm, fieldForm, hookForm, ObjectSchema as ObjectSchema2, FieldSchema, HookSchema } from "@objectstack/spec/data";
1155
+ import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR2, SINGULAR_TO_PLURAL as SINGULAR_TO_PLURAL2 } from "@objectstack/spec/shared";
1156
+ import { ListViewSchema, FormViewSchema, DashboardSchema, AppSchema as AppSchema2, PageSchema, ReportSchema, ActionSchema, reportForm, viewForm, appForm, dashboardForm, actionForm, pageForm } from "@objectstack/spec/ui";
1157
+ import { RoleSchema, roleForm } from "@objectstack/spec/identity";
1158
+ import { PermissionSetSchema, permissionForm } from "@objectstack/spec/security";
1159
+ import { EmailTemplateSchema, emailTemplateForm } from "@objectstack/spec/system";
1160
+ import { ToolSchema, SkillSchema, AgentSchema, agentForm, toolForm, skillForm } from "@objectstack/spec/ai";
1161
+ import { FlowSchema, WorkflowRuleSchema, ApprovalProcessSchema, flowForm, workflowForm, approvalForm } from "@objectstack/spec/automation";
1133
1162
  import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2 } from "@objectstack/spec/kernel";
1163
+ import { z } from "zod";
1164
+ var TYPE_TO_SCHEMA = {
1165
+ object: ObjectSchema2,
1166
+ field: FieldSchema,
1167
+ dashboard: DashboardSchema,
1168
+ app: AppSchema2,
1169
+ page: PageSchema,
1170
+ report: ReportSchema,
1171
+ action: ActionSchema,
1172
+ role: RoleSchema,
1173
+ permission: PermissionSetSchema,
1174
+ profile: PermissionSetSchema,
1175
+ email_template: EmailTemplateSchema,
1176
+ tool: ToolSchema,
1177
+ skill: SkillSchema,
1178
+ agent: AgentSchema,
1179
+ flow: FlowSchema,
1180
+ workflow: WorkflowRuleSchema,
1181
+ approval: ApprovalProcessSchema,
1182
+ hook: HookSchema
1183
+ };
1184
+ var TYPE_TO_FORM = {
1185
+ object: objectForm,
1186
+ field: fieldForm,
1187
+ hook: hookForm,
1188
+ report: reportForm,
1189
+ view: viewForm,
1190
+ app: appForm,
1191
+ dashboard: dashboardForm,
1192
+ role: roleForm,
1193
+ action: actionForm,
1194
+ page: pageForm,
1195
+ agent: agentForm,
1196
+ tool: toolForm,
1197
+ skill: skillForm,
1198
+ flow: flowForm,
1199
+ workflow: workflowForm,
1200
+ approval: approvalForm,
1201
+ permission: permissionForm,
1202
+ profile: permissionForm,
1203
+ email_template: emailTemplateForm
1204
+ };
1205
+ var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
1206
+ function toJsonSchemaSafe(schema) {
1207
+ const cached = _jsonSchemaCache.get(schema);
1208
+ if (cached !== void 0) return cached ?? void 0;
1209
+ try {
1210
+ const result = z.toJSONSchema(schema, { unrepresentable: "any" });
1211
+ _jsonSchemaCache.set(schema, result);
1212
+ return result;
1213
+ } catch {
1214
+ _jsonSchemaCache.set(schema, null);
1215
+ return void 0;
1216
+ }
1217
+ }
1218
+ var HAND_CRAFTED_SCHEMAS = {
1219
+ object: {
1220
+ type: "object",
1221
+ properties: {
1222
+ name: { type: "string" },
1223
+ label: { type: "string" },
1224
+ pluralLabel: { type: "string" },
1225
+ icon: { type: "string" },
1226
+ description: { type: "string" },
1227
+ tags: { type: "array", items: { type: "string" } },
1228
+ active: { type: "boolean", default: true },
1229
+ isSystem: { type: "boolean", default: false },
1230
+ abstract: { type: "boolean", default: false },
1231
+ datasource: { type: "string" },
1232
+ fields: {
1233
+ type: "array",
1234
+ default: [],
1235
+ items: {
1236
+ type: "object",
1237
+ properties: {
1238
+ name: { type: "string" },
1239
+ label: { type: "string" },
1240
+ type: { type: "string" },
1241
+ required: { type: "boolean", default: false },
1242
+ unique: { type: "boolean", default: false },
1243
+ defaultValue: {},
1244
+ description: { type: "string" }
1245
+ },
1246
+ required: ["name", "type"]
1247
+ }
1248
+ },
1249
+ capabilities: { type: "object", additionalProperties: true }
1250
+ },
1251
+ required: ["name"],
1252
+ additionalProperties: true
1253
+ },
1254
+ action: {
1255
+ type: "object",
1256
+ properties: {
1257
+ name: { type: "string" },
1258
+ label: { type: "string" },
1259
+ objectName: { type: "string" },
1260
+ icon: { type: "string" },
1261
+ type: { type: "string", enum: ["url", "flow", "api", "script"] },
1262
+ variant: { type: "string", enum: ["primary", "secondary", "danger", "ghost", "outline"] },
1263
+ target: { type: "string" },
1264
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
1265
+ body: {
1266
+ type: "array",
1267
+ default: [],
1268
+ items: {
1269
+ type: "object",
1270
+ properties: {
1271
+ line: { type: "string" }
1272
+ }
1273
+ }
1274
+ },
1275
+ params: {
1276
+ type: "array",
1277
+ default: [],
1278
+ items: {
1279
+ type: "object",
1280
+ properties: {
1281
+ name: { type: "string" },
1282
+ label: { type: "string" },
1283
+ type: { type: "string" },
1284
+ required: { type: "boolean", default: false }
1285
+ },
1286
+ required: ["name"]
1287
+ }
1288
+ },
1289
+ confirmText: { type: "string" },
1290
+ successMessage: { type: "string" },
1291
+ refreshAfter: { type: "boolean", default: true },
1292
+ locations: {
1293
+ type: "array",
1294
+ default: [],
1295
+ items: {
1296
+ type: "object",
1297
+ properties: {
1298
+ location: { type: "string" }
1299
+ }
1300
+ }
1301
+ },
1302
+ component: { type: "string" },
1303
+ visible: { type: "string" },
1304
+ disabled: { type: "string" },
1305
+ shortcut: { type: "string" },
1306
+ bulkEnabled: { type: "boolean", default: false },
1307
+ aiExposed: { type: "boolean", default: false },
1308
+ recordIdParam: { type: "string" },
1309
+ recordIdField: { type: "string" },
1310
+ bodyShape: { type: "string", enum: ["flat", "nested"] }
1311
+ },
1312
+ required: ["name", "label", "type"],
1313
+ additionalProperties: true
1314
+ }
1315
+ };
1134
1316
  var FORM_VIEW_TYPES = /* @__PURE__ */ new Set(["simple", "tabbed", "wizard", "split", "drawer", "modal"]);
1135
1317
  function resolveOverlaySchema(type, item) {
1136
- const singular = PLURAL_TO_SINGULAR[type] ?? type;
1318
+ const singular = PLURAL_TO_SINGULAR2[type] ?? type;
1137
1319
  switch (singular) {
1138
1320
  case "view": {
1139
1321
  const t = item && typeof item === "object" && "type" in item ? String(item.type) : void 0;
@@ -1190,6 +1372,140 @@ var SERVICE_CONFIG = {
1190
1372
  "file-storage": { route: "/api/v1/storage", plugin: "plugin-storage" },
1191
1373
  search: { route: "/api/v1/search", plugin: "plugin-search" }
1192
1374
  };
1375
+ var REFERENCE_PATHS = {
1376
+ object: [
1377
+ { fromType: "view", paths: ["object", "objectName"], kind: "view" },
1378
+ { fromType: "dashboard", paths: ["widgets[].object", "widgets[].objectName"], kind: "dashboard widget" },
1379
+ { fromType: "flow", paths: ["object", "context.object", "trigger.object", "targetObject"], kind: "flow" },
1380
+ { fromType: "workflow", paths: ["object", "targetObject"], kind: "workflow" },
1381
+ { fromType: "permission", paths: ["objects[].name", "objects[].object"], kind: "permission" },
1382
+ { fromType: "app", paths: ["navItems[].objectName", "navItems[].object", "tabs[].objectName", "tabs[].object"], kind: "app nav" },
1383
+ { fromType: "page", paths: ["object", "objectName"], kind: "page" },
1384
+ { fromType: "report", paths: ["object", "objectName"], kind: "report" },
1385
+ { fromType: "action", paths: ["object", "objectName"], kind: "action" },
1386
+ { fromType: "validation", paths: ["object", "objectName"], kind: "validation" },
1387
+ { fromType: "hook", paths: ["object", "objectName"], kind: "hook" },
1388
+ { fromType: "object", paths: ["fields[].referenceTo", "fields{}.referenceTo", "fields{}.reference"], kind: "field reference" }
1389
+ ],
1390
+ view: [
1391
+ { fromType: "dashboard", paths: ["widgets[].view", "widgets[].viewName"], kind: "dashboard widget" },
1392
+ { fromType: "app", paths: ["navItems[].viewName", "tabs[].viewName"], kind: "app nav" },
1393
+ { fromType: "page", paths: ["viewName"], kind: "page" }
1394
+ ],
1395
+ tool: [
1396
+ { fromType: "agent", paths: ["tools[]", "tools[].name"], kind: "agent tool" }
1397
+ ],
1398
+ skill: [
1399
+ { fromType: "agent", paths: ["skills[]", "skills[].name"], kind: "agent skill" }
1400
+ ],
1401
+ flow: [
1402
+ { fromType: "app", paths: ["navItems[].flowName", "tabs[].flowName"], kind: "app nav" }
1403
+ ],
1404
+ dashboard: [
1405
+ { fromType: "app", paths: ["navItems[].dashboardName", "tabs[].dashboardName"], kind: "app nav" }
1406
+ ],
1407
+ page: [
1408
+ { fromType: "app", paths: ["navItems[].pageName", "tabs[].pageName"], kind: "app nav" }
1409
+ ]
1410
+ };
1411
+ function extractPathValues(item, path) {
1412
+ if (!item || typeof item !== "object") return [];
1413
+ const segments = path.split(".");
1414
+ let current = [item];
1415
+ for (const rawSeg of segments) {
1416
+ let kind = "value";
1417
+ let seg = rawSeg;
1418
+ if (seg.endsWith("[]")) {
1419
+ kind = "array";
1420
+ seg = seg.slice(0, -2);
1421
+ } else if (seg.endsWith("{}")) {
1422
+ kind = "record";
1423
+ seg = seg.slice(0, -2);
1424
+ }
1425
+ const next = [];
1426
+ for (const node of current) {
1427
+ if (!node || typeof node !== "object") continue;
1428
+ let value;
1429
+ if (seg === "") {
1430
+ value = node;
1431
+ } else {
1432
+ value = node[seg];
1433
+ }
1434
+ if (value === void 0 || value === null) continue;
1435
+ if (kind === "array") {
1436
+ if (Array.isArray(value)) {
1437
+ for (const v of value) next.push(v);
1438
+ }
1439
+ } else if (kind === "record") {
1440
+ if (Array.isArray(value)) {
1441
+ for (const v of value) next.push(v);
1442
+ } else if (typeof value === "object") {
1443
+ for (const v of Object.values(value)) next.push(v);
1444
+ }
1445
+ } else {
1446
+ next.push(value);
1447
+ }
1448
+ }
1449
+ current = next;
1450
+ if (current.length === 0) return [];
1451
+ }
1452
+ const out = [];
1453
+ for (const v of current) {
1454
+ if (typeof v === "string" && v.length > 0) out.push(v);
1455
+ else if (v && typeof v === "object" && "name" in v && typeof v.name === "string") {
1456
+ out.push(v.name);
1457
+ }
1458
+ }
1459
+ return out;
1460
+ }
1461
+ function detectDestructiveObjectChanges(prev, next) {
1462
+ if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
1463
+ const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
1464
+ const nextFields = next.fields && typeof next.fields === "object" ? next.fields : {};
1465
+ const issues = [];
1466
+ for (const fname of Object.keys(prevFields)) {
1467
+ if (prevFields[fname]?.system) continue;
1468
+ if (!(fname in nextFields)) {
1469
+ issues.push({
1470
+ code: "field_removed",
1471
+ field: fname,
1472
+ message: `Field '${fname}' removed \u2014 existing data in this column will become inaccessible.`
1473
+ });
1474
+ }
1475
+ }
1476
+ const TYPE_COMPATIBILITY = {
1477
+ text: /* @__PURE__ */ new Set(["textarea", "markdown", "html", "code"]),
1478
+ number: /* @__PURE__ */ new Set([]),
1479
+ boolean: /* @__PURE__ */ new Set([]),
1480
+ date: /* @__PURE__ */ new Set(["datetime"]),
1481
+ datetime: /* @__PURE__ */ new Set(["date"])
1482
+ };
1483
+ for (const fname of Object.keys(nextFields)) {
1484
+ const prevField = prevFields[fname];
1485
+ const nextField = nextFields[fname];
1486
+ if (!prevField) continue;
1487
+ const prevType = prevField.type;
1488
+ const nextType = nextField.type;
1489
+ if (prevType && nextType && prevType !== nextType) {
1490
+ const compatible = TYPE_COMPATIBILITY[prevType]?.has(nextType);
1491
+ if (!compatible) {
1492
+ issues.push({
1493
+ code: "field_type_change",
1494
+ field: fname,
1495
+ message: `Field '${fname}' type changed from '${prevType}' to '${nextType}' \u2014 existing values may not convert cleanly.`
1496
+ });
1497
+ }
1498
+ }
1499
+ if (!prevField.required && nextField.required && nextField.defaultValue === void 0) {
1500
+ issues.push({
1501
+ code: "field_required_no_default",
1502
+ field: fname,
1503
+ message: `Field '${fname}' is now required but has no default value \u2014 existing rows with null values may fail validation.`
1504
+ });
1505
+ }
1506
+ }
1507
+ return issues;
1508
+ }
1193
1509
  var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementation {
1194
1510
  constructor(engine, getServicesRegistry, getFeedService, environmentId) {
1195
1511
  /**
@@ -1391,7 +1707,52 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1391
1707
  } catch {
1392
1708
  }
1393
1709
  const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
1394
- return { types: allTypes };
1710
+ const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
1711
+ const registryByType = new Map(
1712
+ DEFAULT_METADATA_TYPE_REGISTRY2.map((e) => [e.type, e])
1713
+ );
1714
+ const entries = allTypes.map((type) => {
1715
+ const singular = PLURAL_TO_SINGULAR2[type] ?? type;
1716
+ const zodSchema = singular === "view" ? ListViewSchema : TYPE_TO_SCHEMA[singular];
1717
+ const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
1718
+ const form = TYPE_TO_FORM[singular];
1719
+ const base = registryByType.get(singular);
1720
+ if (base) {
1721
+ const isEnvOverridden = writableOverrides.has(singular);
1722
+ return {
1723
+ ...base,
1724
+ type: singular,
1725
+ schemaId: singular,
1726
+ // API client expects schemaId field
1727
+ allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
1728
+ overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
1729
+ schema,
1730
+ form
1731
+ };
1732
+ }
1733
+ return {
1734
+ type: singular,
1735
+ schemaId: singular,
1736
+ // API client expects schemaId field
1737
+ label: singular,
1738
+ description: void 0,
1739
+ filePatterns: [],
1740
+ supportsOverlay: false,
1741
+ allowOrgOverride: writableOverrides.has(singular),
1742
+ allowRuntimeCreate: true,
1743
+ supportsVersioning: false,
1744
+ executionPinned: false,
1745
+ loadOrder: 1e3,
1746
+ domain: "system",
1747
+ overrideSource: writableOverrides.has(singular) ? "env" : "registry",
1748
+ schema,
1749
+ form
1750
+ };
1751
+ }).sort((a, b) => {
1752
+ if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
1753
+ return a.type.localeCompare(b.type);
1754
+ });
1755
+ return { types: allTypes, entries };
1395
1756
  }
1396
1757
  async getMetaItems(request) {
1397
1758
  const { packageId } = request;
@@ -1399,13 +1760,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1399
1760
  if (this.environmentId === void 0) {
1400
1761
  items = [...this.engine.registry.listItems(request.type, packageId)];
1401
1762
  if (items.length === 0) {
1402
- const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
1763
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1403
1764
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1404
1765
  }
1405
1766
  } else {
1406
1767
  items = [...this.engine.registry.listItems(request.type, packageId)];
1407
1768
  if (items.length === 0) {
1408
- const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
1769
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1409
1770
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1410
1771
  }
1411
1772
  }
@@ -1420,7 +1781,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1420
1781
  if (packageId) whereClause._packageId = packageId;
1421
1782
  let rs = await this.engine.find("sys_metadata", { where: whereClause });
1422
1783
  if (!rs || rs.length === 0) {
1423
- const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
1784
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1424
1785
  if (alt) {
1425
1786
  const altWhere = { type: alt, state: "active", organization_id: oid };
1426
1787
  if (packageId) altWhere._packageId = packageId;
@@ -1503,7 +1864,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1503
1864
  };
1504
1865
  const rec = await this.engine.findOne("sys_metadata", { where });
1505
1866
  if (rec) return rec;
1506
- const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
1867
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1507
1868
  if (alt) {
1508
1869
  const altWhere = {
1509
1870
  type: alt,
@@ -1530,7 +1891,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1530
1891
  if (fromService !== void 0 && fromService !== null) {
1531
1892
  item = fromService;
1532
1893
  } else {
1533
- const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
1894
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1534
1895
  if (alt) {
1535
1896
  const altFromService = await metadataService.get(alt, request.name);
1536
1897
  if (altFromService !== void 0 && altFromService !== null) {
@@ -1545,7 +1906,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1545
1906
  if (item === void 0) {
1546
1907
  item = this.engine.registry.getItem(request.type, request.name);
1547
1908
  if (item === void 0) {
1548
- const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];
1909
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1549
1910
  if (alt) item = this.engine.registry.getItem(alt, request.name);
1550
1911
  }
1551
1912
  }
@@ -1555,6 +1916,91 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1555
1916
  item
1556
1917
  };
1557
1918
  }
1919
+ /**
1920
+ * Phase 3a-layered-get: return the 3 layers of a metadata item
1921
+ * separately — `code` (artifact-loaded baseline), `overlay` (per-org
1922
+ * customisation row, if any), and `effective` (what `getMetaItem`
1923
+ * would return, i.e. overlay-wins merge).
1924
+ *
1925
+ * Drives the "Code default vs Overlay vs Effective" diff tab in the
1926
+ * generic Metadata Resource Edit page. Admins can see exactly what
1927
+ * was customised and reset selectively.
1928
+ *
1929
+ * `code` is null if no artifact baseline exists; `overlay` is null if
1930
+ * no sys_metadata row exists for the requested scope; `effective` is
1931
+ * never null when either layer exists.
1932
+ */
1933
+ async getMetaItemLayered(request) {
1934
+ const orgId = request.organizationId;
1935
+ let code = null;
1936
+ try {
1937
+ const services = this.getServicesRegistry?.();
1938
+ const metadataService = services?.get("metadata");
1939
+ if (metadataService && typeof metadataService.get === "function") {
1940
+ let fromService = await metadataService.get(request.type, request.name);
1941
+ if (fromService === void 0 || fromService === null) {
1942
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1943
+ if (alt) fromService = await metadataService.get(alt, request.name);
1944
+ }
1945
+ if (fromService !== void 0 && fromService !== null) code = fromService;
1946
+ }
1947
+ } catch {
1948
+ }
1949
+ if (code === null) {
1950
+ let regItem = this.engine.registry.getItem(request.type, request.name);
1951
+ if (regItem === void 0) {
1952
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1953
+ if (alt) regItem = this.engine.registry.getItem(alt, request.name);
1954
+ }
1955
+ if (regItem !== void 0) code = regItem;
1956
+ }
1957
+ let overlay = null;
1958
+ let overlayScope = null;
1959
+ try {
1960
+ const findOverlay = async (oid) => {
1961
+ const where = {
1962
+ type: request.type,
1963
+ name: request.name,
1964
+ state: "active",
1965
+ organization_id: oid
1966
+ };
1967
+ let rec = await this.engine.findOne("sys_metadata", { where });
1968
+ if (!rec) {
1969
+ const alt = PLURAL_TO_SINGULAR2[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
1970
+ if (alt) {
1971
+ rec = await this.engine.findOne("sys_metadata", {
1972
+ where: { ...where, type: alt }
1973
+ });
1974
+ }
1975
+ }
1976
+ return rec;
1977
+ };
1978
+ if (orgId) {
1979
+ const rec = await findOverlay(orgId);
1980
+ if (rec) {
1981
+ overlay = typeof rec.metadata === "string" ? JSON.parse(rec.metadata) : rec.metadata;
1982
+ overlayScope = "org";
1983
+ }
1984
+ }
1985
+ if (overlay === null) {
1986
+ const rec = await findOverlay(null);
1987
+ if (rec) {
1988
+ overlay = typeof rec.metadata === "string" ? JSON.parse(rec.metadata) : rec.metadata;
1989
+ overlayScope = "env";
1990
+ }
1991
+ }
1992
+ } catch {
1993
+ }
1994
+ const effective = overlay ?? code;
1995
+ return {
1996
+ type: request.type,
1997
+ name: request.name,
1998
+ code,
1999
+ overlay,
2000
+ overlayScope,
2001
+ effective
2002
+ };
2003
+ }
1558
2004
  async getUiView(request) {
1559
2005
  const schema = this.engine.registry.getObject(request.object);
1560
2006
  if (!schema) throw new Error(`Object ${request.object} not found`);
@@ -2426,10 +2872,33 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2426
2872
  ...request.options
2427
2873
  });
2428
2874
  }
2875
+ static envWritableTypes() {
2876
+ if (this._envWritableTypes !== null) return this._envWritableTypes;
2877
+ const raw = typeof process !== "undefined" && process?.env?.OBJECTSTACK_METADATA_WRITABLE || "";
2878
+ const set = /* @__PURE__ */ new Set();
2879
+ for (const tok of raw.split(",")) {
2880
+ const t = tok.trim();
2881
+ if (!t) continue;
2882
+ const singular = PLURAL_TO_SINGULAR2[t] ?? t;
2883
+ set.add(singular);
2884
+ const plural = SINGULAR_TO_PLURAL2[singular];
2885
+ if (plural) set.add(plural);
2886
+ }
2887
+ this._envWritableTypes = set;
2888
+ return set;
2889
+ }
2890
+ /** Test hook — clear the memoised env-writable cache. */
2891
+ static resetEnvWritableCache() {
2892
+ this._envWritableTypes = null;
2893
+ }
2429
2894
  /** Normalize plural→singular before consulting the allow-list. */
2430
2895
  static isOverlayAllowed(type) {
2431
- const singular = PLURAL_TO_SINGULAR[type] ?? type;
2432
- return _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES.has(singular) || _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES.has(type);
2896
+ const singular = PLURAL_TO_SINGULAR2[type] ?? type;
2897
+ if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
2898
+ return true;
2899
+ }
2900
+ const env = this.envWritableTypes();
2901
+ return env.has(singular) || env.has(type);
2433
2902
  }
2434
2903
  /**
2435
2904
  * Mirror an object-type overlay write into the in-memory engine
@@ -2459,12 +2928,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2459
2928
  if (this.environmentId !== void 0 && !_ObjectStackProtocolImplementation.isOverlayAllowed(request.type)) {
2460
2929
  const allowed = Array.from(_ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES).join(", ");
2461
2930
  const err = new Error(
2462
- `[not_overridable] Metadata type '${request.type}' has not opted into per-org overlay writes. Set allowOrgOverride: true on its DEFAULT_METADATA_TYPE_REGISTRY entry to enable. Currently allowed: ${allowed}. See docs/adr/0005-metadata-customization-overlay.md.`
2931
+ `[not_overridable] Metadata type '${request.type}' has not opted into per-org overlay writes. Set allowOrgOverride: true on its DEFAULT_METADATA_TYPE_REGISTRY entry to enable, or set the OBJECTSTACK_METADATA_WRITABLE env var (comma-separated singular type names) to opt in at runtime. Currently allowed: ${allowed}. See docs/adr/0005-metadata-customization-overlay.md.`
2463
2932
  );
2464
2933
  err.code = "not_overridable";
2465
2934
  err.status = 403;
2466
2935
  throw err;
2467
2936
  }
2937
+ const singularType = PLURAL_TO_SINGULAR2[request.type] ?? request.type;
2938
+ if (!request.force && (singularType === "object" || singularType === "field")) {
2939
+ try {
2940
+ const existing = await this.getMetaItem({
2941
+ type: request.type,
2942
+ name: request.name,
2943
+ ...request.organizationId ? { organizationId: request.organizationId } : {}
2944
+ });
2945
+ const prev = existing?.item;
2946
+ if (prev) {
2947
+ const issues = detectDestructiveObjectChanges(prev, request.item);
2948
+ if (issues.length > 0) {
2949
+ const summary = issues.slice(0, 3).map((i) => i.message).join("; ");
2950
+ const err = new Error(
2951
+ `[destructive_change] ${request.type}/${request.name} would drop or transform existing data: ${summary}` + (issues.length > 3 ? ` (+${issues.length - 3} more)` : "") + ` \u2014 re-submit with ?force=true to proceed.`
2952
+ );
2953
+ err.code = "destructive_change";
2954
+ err.status = 409;
2955
+ err.issues = issues;
2956
+ throw err;
2957
+ }
2958
+ }
2959
+ } catch (err) {
2960
+ if (err?.code === "destructive_change") throw err;
2961
+ }
2962
+ }
2468
2963
  {
2469
2964
  const schema = resolveOverlaySchema(request.type, request.item);
2470
2965
  if (schema) {
@@ -2487,7 +2982,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2487
2982
  }
2488
2983
  }
2489
2984
  await this.ensureOverlayIndex();
2490
- const singularTypeForRepo = PLURAL_TO_SINGULAR[request.type] ?? request.type;
2985
+ const singularTypeForRepo = PLURAL_TO_SINGULAR2[request.type] ?? request.type;
2491
2986
  if (_ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo)) {
2492
2987
  const orgId = request.organizationId ?? null;
2493
2988
  const repo = this.getOverlayRepo(orgId);
@@ -2598,7 +3093,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2598
3093
  * "no history" uniformly.
2599
3094
  */
2600
3095
  async historyMetaItem(request) {
2601
- const singularType = PLURAL_TO_SINGULAR[request.type] ?? request.type;
3096
+ const singularType = PLURAL_TO_SINGULAR2[request.type] ?? request.type;
2602
3097
  if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
2603
3098
  return { events: [] };
2604
3099
  }
@@ -2631,7 +3126,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2631
3126
  err.status = 403;
2632
3127
  throw err;
2633
3128
  }
2634
- const singularTypeForRepo = PLURAL_TO_SINGULAR[request.type] ?? request.type;
3129
+ const singularTypeForRepo = PLURAL_TO_SINGULAR2[request.type] ?? request.type;
2635
3130
  const useRepoPath = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
2636
3131
  if (useRepoPath) {
2637
3132
  const orgId = request.organizationId ?? null;
@@ -2751,7 +3246,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2751
3246
  for (const record of records) {
2752
3247
  try {
2753
3248
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2754
- const normalizedType = PLURAL_TO_SINGULAR[record.type] ?? record.type;
3249
+ const normalizedType = PLURAL_TO_SINGULAR2[record.type] ?? record.type;
2755
3250
  if (normalizedType === "object") {
2756
3251
  this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
2757
3252
  } else {
@@ -2771,6 +3266,68 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2771
3266
  return { loaded, errors };
2772
3267
  }
2773
3268
  // ==========================================
3269
+ // Metadata References (Phase 3a-references)
3270
+ // ==========================================
3271
+ /**
3272
+ * Scan all loaded metadata for references pointing at the given
3273
+ * `{type, name}` target. Returns one row per referring artifact with
3274
+ * the path that produced the hit, so the admin UI can render an
3275
+ * "Used by" panel before destructive actions (rename / delete /
3276
+ * type-narrowing).
3277
+ *
3278
+ * Coverage is driven by the hand-curated {@link REFERENCE_PATHS}
3279
+ * registry. Types not present in the registry simply return no hits
3280
+ * — the engine never throws.
3281
+ */
3282
+ async findReferencesToMeta(request) {
3283
+ const singularTarget = PLURAL_TO_SINGULAR2[request.type] ?? request.type;
3284
+ const targetName = request.name;
3285
+ const matchers = REFERENCE_PATHS[singularTarget];
3286
+ if (!matchers || matchers.length === 0) {
3287
+ return { references: [] };
3288
+ }
3289
+ const seen = /* @__PURE__ */ new Set();
3290
+ const out = [];
3291
+ await Promise.all(
3292
+ matchers.map(async (matcher) => {
3293
+ let items = [];
3294
+ try {
3295
+ const result = await this.getMetaItems({
3296
+ type: matcher.fromType,
3297
+ ...request.organizationId ? { organizationId: request.organizationId } : {}
3298
+ });
3299
+ items = result?.items ?? [];
3300
+ } catch {
3301
+ return;
3302
+ }
3303
+ for (const raw of items) {
3304
+ if (!raw || typeof raw !== "object") continue;
3305
+ const sourceName = raw.name;
3306
+ if (!sourceName) continue;
3307
+ const isSelfReference = matcher.fromType === singularTarget && sourceName === targetName;
3308
+ for (const path of matcher.paths) {
3309
+ const values = extractPathValues(raw, path);
3310
+ if (!values.includes(targetName)) continue;
3311
+ if (isSelfReference && !path.includes("[]") && !path.includes("{}")) continue;
3312
+ const key = `${matcher.fromType}|${sourceName}|${path}`;
3313
+ if (seen.has(key)) continue;
3314
+ seen.add(key);
3315
+ const label = raw.label;
3316
+ out.push({
3317
+ type: matcher.fromType,
3318
+ name: sourceName,
3319
+ ...label ? { label } : {},
3320
+ path,
3321
+ kind: matcher.kind
3322
+ });
3323
+ }
3324
+ }
3325
+ })
3326
+ );
3327
+ out.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
3328
+ return { references: out };
3329
+ }
3330
+ // ==========================================
2774
3331
  // Feed Operations
2775
3332
  // ==========================================
2776
3333
  async listFeed(request) {
@@ -2917,11 +3474,23 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
2917
3474
  for (const entry of DEFAULT_METADATA_TYPE_REGISTRY2) {
2918
3475
  if (!entry.allowOrgOverride) continue;
2919
3476
  out.add(entry.type);
2920
- const plural = SINGULAR_TO_PLURAL[entry.type];
3477
+ const plural = SINGULAR_TO_PLURAL2[entry.type];
2921
3478
  if (plural) out.add(plural);
2922
3479
  }
2923
3480
  return out;
2924
3481
  })();
3482
+ /**
3483
+ * Phase 3a-env-writable: parse `OBJECTSTACK_METADATA_WRITABLE` once.
3484
+ * Comma-separated singular type names. When the env var is set, the
3485
+ * listed types get treated as `allowOrgOverride: true` regardless of
3486
+ * their static registry entry. This is the runtime escape hatch admins
3487
+ * use to enable Studio-side editing of types whose protocol-level flag
3488
+ * is still false (object, field, permission, …).
3489
+ *
3490
+ * Memoised at first call. Tests can override by clearing the cache via
3491
+ * {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
3492
+ */
3493
+ _ObjectStackProtocolImplementation._envWritableTypes = null;
2925
3494
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
2926
3495
 
2927
3496
  // src/engine.ts