@objectstack/objectql 6.7.0 → 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.js CHANGED
@@ -672,9 +672,26 @@ var SchemaRegistry = class {
672
672
  // src/sys-metadata-repository.ts
673
673
  var import_metadata_core = require("@objectstack/metadata-core");
674
674
  var import_kernel2 = require("@objectstack/spec/kernel");
675
+ var import_shared = require("@objectstack/spec/shared");
675
676
  var OVERLAY_ALLOWED_TYPES = new Set(
676
677
  import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
677
678
  );
679
+ var _envWritableMetadataTypes = null;
680
+ function envWritableMetadataTypes() {
681
+ if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
682
+ const raw = typeof process !== "undefined" && process?.env?.OBJECTSTACK_METADATA_WRITABLE || "";
683
+ const set = /* @__PURE__ */ new Set();
684
+ for (const tok of raw.split(",")) {
685
+ const t = tok.trim();
686
+ if (!t) continue;
687
+ const singular = import_shared.PLURAL_TO_SINGULAR[t] ?? t;
688
+ set.add(singular);
689
+ const plural = import_shared.SINGULAR_TO_PLURAL[singular];
690
+ if (plural) set.add(plural);
691
+ }
692
+ _envWritableMetadataTypes = set;
693
+ return set;
694
+ }
678
695
  var SysMetadataRepository = class {
679
696
  constructor(opts) {
680
697
  /**
@@ -1067,14 +1084,21 @@ var SysMetadataRepository = class {
1067
1084
  if (this.closed) throw new Error("SysMetadataRepository is closed");
1068
1085
  }
1069
1086
  assertAllowed(type) {
1070
- if (!OVERLAY_ALLOWED_TYPES.has(type)) {
1071
- const err = new Error(
1072
- `[not_overridable] '${type}' is not allowOrgOverride in the registry. Allowed: ${Array.from(OVERLAY_ALLOWED_TYPES).join(", ")}.`
1073
- );
1074
- err.code = "not_overridable";
1075
- err.status = 403;
1076
- throw err;
1077
- }
1087
+ const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
1088
+ const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
1089
+ if (allowedByRegistry) return;
1090
+ const env = envWritableMetadataTypes();
1091
+ if (env.has(singular) || env.has(type)) return;
1092
+ const allowed = [
1093
+ ...OVERLAY_ALLOWED_TYPES,
1094
+ ...envWritableMetadataTypes()
1095
+ ];
1096
+ const err = new Error(
1097
+ `[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.`
1098
+ );
1099
+ err.code = "not_overridable";
1100
+ err.status = 403;
1101
+ throw err;
1078
1102
  }
1079
1103
  whereFor(ref) {
1080
1104
  return {
@@ -1178,12 +1202,170 @@ var SysMetadataRepository = class {
1178
1202
  // src/protocol.ts
1179
1203
  var import_metadata_core2 = require("@objectstack/metadata-core");
1180
1204
  var import_data2 = require("@objectstack/spec/data");
1181
- var import_shared = require("@objectstack/spec/shared");
1205
+ var import_shared2 = require("@objectstack/spec/shared");
1182
1206
  var import_ui2 = require("@objectstack/spec/ui");
1207
+ var import_identity = require("@objectstack/spec/identity");
1208
+ var import_security = require("@objectstack/spec/security");
1209
+ var import_system = require("@objectstack/spec/system");
1210
+ var import_ai = require("@objectstack/spec/ai");
1211
+ var import_automation = require("@objectstack/spec/automation");
1183
1212
  var import_kernel3 = require("@objectstack/spec/kernel");
1213
+ var import_zod = require("zod");
1214
+ var TYPE_TO_SCHEMA = {
1215
+ object: import_data2.ObjectSchema,
1216
+ field: import_data2.FieldSchema,
1217
+ dashboard: import_ui2.DashboardSchema,
1218
+ app: import_ui2.AppSchema,
1219
+ page: import_ui2.PageSchema,
1220
+ report: import_ui2.ReportSchema,
1221
+ action: import_ui2.ActionSchema,
1222
+ role: import_identity.RoleSchema,
1223
+ permission: import_security.PermissionSetSchema,
1224
+ profile: import_security.PermissionSetSchema,
1225
+ email_template: import_system.EmailTemplateSchema,
1226
+ tool: import_ai.ToolSchema,
1227
+ skill: import_ai.SkillSchema,
1228
+ agent: import_ai.AgentSchema,
1229
+ flow: import_automation.FlowSchema,
1230
+ workflow: import_automation.WorkflowRuleSchema,
1231
+ approval: import_automation.ApprovalProcessSchema,
1232
+ hook: import_data2.HookSchema
1233
+ };
1234
+ var TYPE_TO_FORM = {
1235
+ object: import_data2.objectForm,
1236
+ field: import_data2.fieldForm,
1237
+ hook: import_data2.hookForm,
1238
+ report: import_ui2.reportForm,
1239
+ view: import_ui2.viewForm,
1240
+ app: import_ui2.appForm,
1241
+ dashboard: import_ui2.dashboardForm,
1242
+ role: import_identity.roleForm,
1243
+ action: import_ui2.actionForm,
1244
+ page: import_ui2.pageForm,
1245
+ agent: import_ai.agentForm,
1246
+ tool: import_ai.toolForm,
1247
+ skill: import_ai.skillForm,
1248
+ flow: import_automation.flowForm,
1249
+ workflow: import_automation.workflowForm,
1250
+ approval: import_automation.approvalForm,
1251
+ permission: import_security.permissionForm,
1252
+ profile: import_security.permissionForm,
1253
+ email_template: import_system.emailTemplateForm
1254
+ };
1255
+ var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
1256
+ function toJsonSchemaSafe(schema) {
1257
+ const cached = _jsonSchemaCache.get(schema);
1258
+ if (cached !== void 0) return cached ?? void 0;
1259
+ try {
1260
+ const result = import_zod.z.toJSONSchema(schema, { unrepresentable: "any" });
1261
+ _jsonSchemaCache.set(schema, result);
1262
+ return result;
1263
+ } catch {
1264
+ _jsonSchemaCache.set(schema, null);
1265
+ return void 0;
1266
+ }
1267
+ }
1268
+ var HAND_CRAFTED_SCHEMAS = {
1269
+ object: {
1270
+ type: "object",
1271
+ properties: {
1272
+ name: { type: "string" },
1273
+ label: { type: "string" },
1274
+ pluralLabel: { type: "string" },
1275
+ icon: { type: "string" },
1276
+ description: { type: "string" },
1277
+ tags: { type: "array", items: { type: "string" } },
1278
+ active: { type: "boolean", default: true },
1279
+ isSystem: { type: "boolean", default: false },
1280
+ abstract: { type: "boolean", default: false },
1281
+ datasource: { type: "string" },
1282
+ fields: {
1283
+ type: "array",
1284
+ default: [],
1285
+ items: {
1286
+ type: "object",
1287
+ properties: {
1288
+ name: { type: "string" },
1289
+ label: { type: "string" },
1290
+ type: { type: "string" },
1291
+ required: { type: "boolean", default: false },
1292
+ unique: { type: "boolean", default: false },
1293
+ defaultValue: {},
1294
+ description: { type: "string" }
1295
+ },
1296
+ required: ["name", "type"]
1297
+ }
1298
+ },
1299
+ capabilities: { type: "object", additionalProperties: true }
1300
+ },
1301
+ required: ["name"],
1302
+ additionalProperties: true
1303
+ },
1304
+ action: {
1305
+ type: "object",
1306
+ properties: {
1307
+ name: { type: "string" },
1308
+ label: { type: "string" },
1309
+ objectName: { type: "string" },
1310
+ icon: { type: "string" },
1311
+ type: { type: "string", enum: ["url", "flow", "api", "script"] },
1312
+ variant: { type: "string", enum: ["primary", "secondary", "danger", "ghost", "outline"] },
1313
+ target: { type: "string" },
1314
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
1315
+ body: {
1316
+ type: "array",
1317
+ default: [],
1318
+ items: {
1319
+ type: "object",
1320
+ properties: {
1321
+ line: { type: "string" }
1322
+ }
1323
+ }
1324
+ },
1325
+ params: {
1326
+ type: "array",
1327
+ default: [],
1328
+ items: {
1329
+ type: "object",
1330
+ properties: {
1331
+ name: { type: "string" },
1332
+ label: { type: "string" },
1333
+ type: { type: "string" },
1334
+ required: { type: "boolean", default: false }
1335
+ },
1336
+ required: ["name"]
1337
+ }
1338
+ },
1339
+ confirmText: { type: "string" },
1340
+ successMessage: { type: "string" },
1341
+ refreshAfter: { type: "boolean", default: true },
1342
+ locations: {
1343
+ type: "array",
1344
+ default: [],
1345
+ items: {
1346
+ type: "object",
1347
+ properties: {
1348
+ location: { type: "string" }
1349
+ }
1350
+ }
1351
+ },
1352
+ component: { type: "string" },
1353
+ visible: { type: "string" },
1354
+ disabled: { type: "string" },
1355
+ shortcut: { type: "string" },
1356
+ bulkEnabled: { type: "boolean", default: false },
1357
+ aiExposed: { type: "boolean", default: false },
1358
+ recordIdParam: { type: "string" },
1359
+ recordIdField: { type: "string" },
1360
+ bodyShape: { type: "string", enum: ["flat", "nested"] }
1361
+ },
1362
+ required: ["name", "label", "type"],
1363
+ additionalProperties: true
1364
+ }
1365
+ };
1184
1366
  var FORM_VIEW_TYPES = /* @__PURE__ */ new Set(["simple", "tabbed", "wizard", "split", "drawer", "modal"]);
1185
1367
  function resolveOverlaySchema(type, item) {
1186
- const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
1368
+ const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1187
1369
  switch (singular) {
1188
1370
  case "view": {
1189
1371
  const t = item && typeof item === "object" && "type" in item ? String(item.type) : void 0;
@@ -1240,6 +1422,140 @@ var SERVICE_CONFIG = {
1240
1422
  "file-storage": { route: "/api/v1/storage", plugin: "plugin-storage" },
1241
1423
  search: { route: "/api/v1/search", plugin: "plugin-search" }
1242
1424
  };
1425
+ var REFERENCE_PATHS = {
1426
+ object: [
1427
+ { fromType: "view", paths: ["object", "objectName"], kind: "view" },
1428
+ { fromType: "dashboard", paths: ["widgets[].object", "widgets[].objectName"], kind: "dashboard widget" },
1429
+ { fromType: "flow", paths: ["object", "context.object", "trigger.object", "targetObject"], kind: "flow" },
1430
+ { fromType: "workflow", paths: ["object", "targetObject"], kind: "workflow" },
1431
+ { fromType: "permission", paths: ["objects[].name", "objects[].object"], kind: "permission" },
1432
+ { fromType: "app", paths: ["navItems[].objectName", "navItems[].object", "tabs[].objectName", "tabs[].object"], kind: "app nav" },
1433
+ { fromType: "page", paths: ["object", "objectName"], kind: "page" },
1434
+ { fromType: "report", paths: ["object", "objectName"], kind: "report" },
1435
+ { fromType: "action", paths: ["object", "objectName"], kind: "action" },
1436
+ { fromType: "validation", paths: ["object", "objectName"], kind: "validation" },
1437
+ { fromType: "hook", paths: ["object", "objectName"], kind: "hook" },
1438
+ { fromType: "object", paths: ["fields[].referenceTo", "fields{}.referenceTo", "fields{}.reference"], kind: "field reference" }
1439
+ ],
1440
+ view: [
1441
+ { fromType: "dashboard", paths: ["widgets[].view", "widgets[].viewName"], kind: "dashboard widget" },
1442
+ { fromType: "app", paths: ["navItems[].viewName", "tabs[].viewName"], kind: "app nav" },
1443
+ { fromType: "page", paths: ["viewName"], kind: "page" }
1444
+ ],
1445
+ tool: [
1446
+ { fromType: "agent", paths: ["tools[]", "tools[].name"], kind: "agent tool" }
1447
+ ],
1448
+ skill: [
1449
+ { fromType: "agent", paths: ["skills[]", "skills[].name"], kind: "agent skill" }
1450
+ ],
1451
+ flow: [
1452
+ { fromType: "app", paths: ["navItems[].flowName", "tabs[].flowName"], kind: "app nav" }
1453
+ ],
1454
+ dashboard: [
1455
+ { fromType: "app", paths: ["navItems[].dashboardName", "tabs[].dashboardName"], kind: "app nav" }
1456
+ ],
1457
+ page: [
1458
+ { fromType: "app", paths: ["navItems[].pageName", "tabs[].pageName"], kind: "app nav" }
1459
+ ]
1460
+ };
1461
+ function extractPathValues(item, path) {
1462
+ if (!item || typeof item !== "object") return [];
1463
+ const segments = path.split(".");
1464
+ let current = [item];
1465
+ for (const rawSeg of segments) {
1466
+ let kind = "value";
1467
+ let seg = rawSeg;
1468
+ if (seg.endsWith("[]")) {
1469
+ kind = "array";
1470
+ seg = seg.slice(0, -2);
1471
+ } else if (seg.endsWith("{}")) {
1472
+ kind = "record";
1473
+ seg = seg.slice(0, -2);
1474
+ }
1475
+ const next = [];
1476
+ for (const node of current) {
1477
+ if (!node || typeof node !== "object") continue;
1478
+ let value;
1479
+ if (seg === "") {
1480
+ value = node;
1481
+ } else {
1482
+ value = node[seg];
1483
+ }
1484
+ if (value === void 0 || value === null) continue;
1485
+ if (kind === "array") {
1486
+ if (Array.isArray(value)) {
1487
+ for (const v of value) next.push(v);
1488
+ }
1489
+ } else if (kind === "record") {
1490
+ if (Array.isArray(value)) {
1491
+ for (const v of value) next.push(v);
1492
+ } else if (typeof value === "object") {
1493
+ for (const v of Object.values(value)) next.push(v);
1494
+ }
1495
+ } else {
1496
+ next.push(value);
1497
+ }
1498
+ }
1499
+ current = next;
1500
+ if (current.length === 0) return [];
1501
+ }
1502
+ const out = [];
1503
+ for (const v of current) {
1504
+ if (typeof v === "string" && v.length > 0) out.push(v);
1505
+ else if (v && typeof v === "object" && "name" in v && typeof v.name === "string") {
1506
+ out.push(v.name);
1507
+ }
1508
+ }
1509
+ return out;
1510
+ }
1511
+ function detectDestructiveObjectChanges(prev, next) {
1512
+ if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
1513
+ const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
1514
+ const nextFields = next.fields && typeof next.fields === "object" ? next.fields : {};
1515
+ const issues = [];
1516
+ for (const fname of Object.keys(prevFields)) {
1517
+ if (prevFields[fname]?.system) continue;
1518
+ if (!(fname in nextFields)) {
1519
+ issues.push({
1520
+ code: "field_removed",
1521
+ field: fname,
1522
+ message: `Field '${fname}' removed \u2014 existing data in this column will become inaccessible.`
1523
+ });
1524
+ }
1525
+ }
1526
+ const TYPE_COMPATIBILITY = {
1527
+ text: /* @__PURE__ */ new Set(["textarea", "markdown", "html", "code"]),
1528
+ number: /* @__PURE__ */ new Set([]),
1529
+ boolean: /* @__PURE__ */ new Set([]),
1530
+ date: /* @__PURE__ */ new Set(["datetime"]),
1531
+ datetime: /* @__PURE__ */ new Set(["date"])
1532
+ };
1533
+ for (const fname of Object.keys(nextFields)) {
1534
+ const prevField = prevFields[fname];
1535
+ const nextField = nextFields[fname];
1536
+ if (!prevField) continue;
1537
+ const prevType = prevField.type;
1538
+ const nextType = nextField.type;
1539
+ if (prevType && nextType && prevType !== nextType) {
1540
+ const compatible = TYPE_COMPATIBILITY[prevType]?.has(nextType);
1541
+ if (!compatible) {
1542
+ issues.push({
1543
+ code: "field_type_change",
1544
+ field: fname,
1545
+ message: `Field '${fname}' type changed from '${prevType}' to '${nextType}' \u2014 existing values may not convert cleanly.`
1546
+ });
1547
+ }
1548
+ }
1549
+ if (!prevField.required && nextField.required && nextField.defaultValue === void 0) {
1550
+ issues.push({
1551
+ code: "field_required_no_default",
1552
+ field: fname,
1553
+ message: `Field '${fname}' is now required but has no default value \u2014 existing rows with null values may fail validation.`
1554
+ });
1555
+ }
1556
+ }
1557
+ return issues;
1558
+ }
1243
1559
  var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementation {
1244
1560
  constructor(engine, getServicesRegistry, getFeedService, environmentId) {
1245
1561
  /**
@@ -1441,7 +1757,52 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1441
1757
  } catch {
1442
1758
  }
1443
1759
  const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
1444
- return { types: allTypes };
1760
+ const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
1761
+ const registryByType = new Map(
1762
+ import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
1763
+ );
1764
+ const entries = allTypes.map((type) => {
1765
+ const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
1766
+ const zodSchema = singular === "view" ? import_ui2.ListViewSchema : TYPE_TO_SCHEMA[singular];
1767
+ const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
1768
+ const form = TYPE_TO_FORM[singular];
1769
+ const base = registryByType.get(singular);
1770
+ if (base) {
1771
+ const isEnvOverridden = writableOverrides.has(singular);
1772
+ return {
1773
+ ...base,
1774
+ type: singular,
1775
+ schemaId: singular,
1776
+ // API client expects schemaId field
1777
+ allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
1778
+ overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
1779
+ schema,
1780
+ form
1781
+ };
1782
+ }
1783
+ return {
1784
+ type: singular,
1785
+ schemaId: singular,
1786
+ // API client expects schemaId field
1787
+ label: singular,
1788
+ description: void 0,
1789
+ filePatterns: [],
1790
+ supportsOverlay: false,
1791
+ allowOrgOverride: writableOverrides.has(singular),
1792
+ allowRuntimeCreate: true,
1793
+ supportsVersioning: false,
1794
+ executionPinned: false,
1795
+ loadOrder: 1e3,
1796
+ domain: "system",
1797
+ overrideSource: writableOverrides.has(singular) ? "env" : "registry",
1798
+ schema,
1799
+ form
1800
+ };
1801
+ }).sort((a, b) => {
1802
+ if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
1803
+ return a.type.localeCompare(b.type);
1804
+ });
1805
+ return { types: allTypes, entries };
1445
1806
  }
1446
1807
  async getMetaItems(request) {
1447
1808
  const { packageId } = request;
@@ -1449,13 +1810,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1449
1810
  if (this.environmentId === void 0) {
1450
1811
  items = [...this.engine.registry.listItems(request.type, packageId)];
1451
1812
  if (items.length === 0) {
1452
- const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1813
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
1453
1814
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1454
1815
  }
1455
1816
  } else {
1456
1817
  items = [...this.engine.registry.listItems(request.type, packageId)];
1457
1818
  if (items.length === 0) {
1458
- const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1819
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
1459
1820
  if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
1460
1821
  }
1461
1822
  }
@@ -1470,7 +1831,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1470
1831
  if (packageId) whereClause._packageId = packageId;
1471
1832
  let rs = await this.engine.find("sys_metadata", { where: whereClause });
1472
1833
  if (!rs || rs.length === 0) {
1473
- const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1834
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
1474
1835
  if (alt) {
1475
1836
  const altWhere = { type: alt, state: "active", organization_id: oid };
1476
1837
  if (packageId) altWhere._packageId = packageId;
@@ -1553,7 +1914,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1553
1914
  };
1554
1915
  const rec = await this.engine.findOne("sys_metadata", { where });
1555
1916
  if (rec) return rec;
1556
- const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1917
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
1557
1918
  if (alt) {
1558
1919
  const altWhere = {
1559
1920
  type: alt,
@@ -1580,7 +1941,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1580
1941
  if (fromService !== void 0 && fromService !== null) {
1581
1942
  item = fromService;
1582
1943
  } else {
1583
- const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1944
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
1584
1945
  if (alt) {
1585
1946
  const altFromService = await metadataService.get(alt, request.name);
1586
1947
  if (altFromService !== void 0 && altFromService !== null) {
@@ -1595,7 +1956,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1595
1956
  if (item === void 0) {
1596
1957
  item = this.engine.registry.getItem(request.type, request.name);
1597
1958
  if (item === void 0) {
1598
- const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
1959
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
1599
1960
  if (alt) item = this.engine.registry.getItem(alt, request.name);
1600
1961
  }
1601
1962
  }
@@ -1605,6 +1966,91 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
1605
1966
  item
1606
1967
  };
1607
1968
  }
1969
+ /**
1970
+ * Phase 3a-layered-get: return the 3 layers of a metadata item
1971
+ * separately — `code` (artifact-loaded baseline), `overlay` (per-org
1972
+ * customisation row, if any), and `effective` (what `getMetaItem`
1973
+ * would return, i.e. overlay-wins merge).
1974
+ *
1975
+ * Drives the "Code default vs Overlay vs Effective" diff tab in the
1976
+ * generic Metadata Resource Edit page. Admins can see exactly what
1977
+ * was customised and reset selectively.
1978
+ *
1979
+ * `code` is null if no artifact baseline exists; `overlay` is null if
1980
+ * no sys_metadata row exists for the requested scope; `effective` is
1981
+ * never null when either layer exists.
1982
+ */
1983
+ async getMetaItemLayered(request) {
1984
+ const orgId = request.organizationId;
1985
+ let code = null;
1986
+ try {
1987
+ const services = this.getServicesRegistry?.();
1988
+ const metadataService = services?.get("metadata");
1989
+ if (metadataService && typeof metadataService.get === "function") {
1990
+ let fromService = await metadataService.get(request.type, request.name);
1991
+ if (fromService === void 0 || fromService === null) {
1992
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
1993
+ if (alt) fromService = await metadataService.get(alt, request.name);
1994
+ }
1995
+ if (fromService !== void 0 && fromService !== null) code = fromService;
1996
+ }
1997
+ } catch {
1998
+ }
1999
+ if (code === null) {
2000
+ let regItem = this.engine.registry.getItem(request.type, request.name);
2001
+ if (regItem === void 0) {
2002
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2003
+ if (alt) regItem = this.engine.registry.getItem(alt, request.name);
2004
+ }
2005
+ if (regItem !== void 0) code = regItem;
2006
+ }
2007
+ let overlay = null;
2008
+ let overlayScope = null;
2009
+ try {
2010
+ const findOverlay = async (oid) => {
2011
+ const where = {
2012
+ type: request.type,
2013
+ name: request.name,
2014
+ state: "active",
2015
+ organization_id: oid
2016
+ };
2017
+ let rec = await this.engine.findOne("sys_metadata", { where });
2018
+ if (!rec) {
2019
+ const alt = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? import_shared2.SINGULAR_TO_PLURAL[request.type];
2020
+ if (alt) {
2021
+ rec = await this.engine.findOne("sys_metadata", {
2022
+ where: { ...where, type: alt }
2023
+ });
2024
+ }
2025
+ }
2026
+ return rec;
2027
+ };
2028
+ if (orgId) {
2029
+ const rec = await findOverlay(orgId);
2030
+ if (rec) {
2031
+ overlay = typeof rec.metadata === "string" ? JSON.parse(rec.metadata) : rec.metadata;
2032
+ overlayScope = "org";
2033
+ }
2034
+ }
2035
+ if (overlay === null) {
2036
+ const rec = await findOverlay(null);
2037
+ if (rec) {
2038
+ overlay = typeof rec.metadata === "string" ? JSON.parse(rec.metadata) : rec.metadata;
2039
+ overlayScope = "env";
2040
+ }
2041
+ }
2042
+ } catch {
2043
+ }
2044
+ const effective = overlay ?? code;
2045
+ return {
2046
+ type: request.type,
2047
+ name: request.name,
2048
+ code,
2049
+ overlay,
2050
+ overlayScope,
2051
+ effective
2052
+ };
2053
+ }
1608
2054
  async getUiView(request) {
1609
2055
  const schema = this.engine.registry.getObject(request.object);
1610
2056
  if (!schema) throw new Error(`Object ${request.object} not found`);
@@ -2476,10 +2922,33 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2476
2922
  ...request.options
2477
2923
  });
2478
2924
  }
2925
+ static envWritableTypes() {
2926
+ if (this._envWritableTypes !== null) return this._envWritableTypes;
2927
+ const raw = typeof process !== "undefined" && process?.env?.OBJECTSTACK_METADATA_WRITABLE || "";
2928
+ const set = /* @__PURE__ */ new Set();
2929
+ for (const tok of raw.split(",")) {
2930
+ const t = tok.trim();
2931
+ if (!t) continue;
2932
+ const singular = import_shared2.PLURAL_TO_SINGULAR[t] ?? t;
2933
+ set.add(singular);
2934
+ const plural = import_shared2.SINGULAR_TO_PLURAL[singular];
2935
+ if (plural) set.add(plural);
2936
+ }
2937
+ this._envWritableTypes = set;
2938
+ return set;
2939
+ }
2940
+ /** Test hook — clear the memoised env-writable cache. */
2941
+ static resetEnvWritableCache() {
2942
+ this._envWritableTypes = null;
2943
+ }
2479
2944
  /** Normalize plural→singular before consulting the allow-list. */
2480
2945
  static isOverlayAllowed(type) {
2481
- const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
2482
- return _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES.has(singular) || _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES.has(type);
2946
+ const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
2947
+ if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
2948
+ return true;
2949
+ }
2950
+ const env = this.envWritableTypes();
2951
+ return env.has(singular) || env.has(type);
2483
2952
  }
2484
2953
  /**
2485
2954
  * Mirror an object-type overlay write into the in-memory engine
@@ -2509,12 +2978,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2509
2978
  if (this.environmentId !== void 0 && !_ObjectStackProtocolImplementation.isOverlayAllowed(request.type)) {
2510
2979
  const allowed = Array.from(_ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES).join(", ");
2511
2980
  const err = new Error(
2512
- `[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.`
2981
+ `[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.`
2513
2982
  );
2514
2983
  err.code = "not_overridable";
2515
2984
  err.status = 403;
2516
2985
  throw err;
2517
2986
  }
2987
+ const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
2988
+ if (!request.force && (singularType === "object" || singularType === "field")) {
2989
+ try {
2990
+ const existing = await this.getMetaItem({
2991
+ type: request.type,
2992
+ name: request.name,
2993
+ ...request.organizationId ? { organizationId: request.organizationId } : {}
2994
+ });
2995
+ const prev = existing?.item;
2996
+ if (prev) {
2997
+ const issues = detectDestructiveObjectChanges(prev, request.item);
2998
+ if (issues.length > 0) {
2999
+ const summary = issues.slice(0, 3).map((i) => i.message).join("; ");
3000
+ const err = new Error(
3001
+ `[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.`
3002
+ );
3003
+ err.code = "destructive_change";
3004
+ err.status = 409;
3005
+ err.issues = issues;
3006
+ throw err;
3007
+ }
3008
+ }
3009
+ } catch (err) {
3010
+ if (err?.code === "destructive_change") throw err;
3011
+ }
3012
+ }
2518
3013
  {
2519
3014
  const schema = resolveOverlaySchema(request.type, request.item);
2520
3015
  if (schema) {
@@ -2537,7 +3032,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2537
3032
  }
2538
3033
  }
2539
3034
  await this.ensureOverlayIndex();
2540
- const singularTypeForRepo = import_shared.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3035
+ const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
2541
3036
  if (_ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo)) {
2542
3037
  const orgId = request.organizationId ?? null;
2543
3038
  const repo = this.getOverlayRepo(orgId);
@@ -2648,7 +3143,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2648
3143
  * "no history" uniformly.
2649
3144
  */
2650
3145
  async historyMetaItem(request) {
2651
- const singularType = import_shared.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3146
+ const singularType = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
2652
3147
  if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
2653
3148
  return { events: [] };
2654
3149
  }
@@ -2681,7 +3176,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2681
3176
  err.status = 403;
2682
3177
  throw err;
2683
3178
  }
2684
- const singularTypeForRepo = import_shared.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3179
+ const singularTypeForRepo = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
2685
3180
  const useRepoPath = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
2686
3181
  if (useRepoPath) {
2687
3182
  const orgId = request.organizationId ?? null;
@@ -2801,7 +3296,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2801
3296
  for (const record of records) {
2802
3297
  try {
2803
3298
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2804
- const normalizedType = import_shared.PLURAL_TO_SINGULAR[record.type] ?? record.type;
3299
+ const normalizedType = import_shared2.PLURAL_TO_SINGULAR[record.type] ?? record.type;
2805
3300
  if (normalizedType === "object") {
2806
3301
  this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
2807
3302
  } else {
@@ -2821,6 +3316,68 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2821
3316
  return { loaded, errors };
2822
3317
  }
2823
3318
  // ==========================================
3319
+ // Metadata References (Phase 3a-references)
3320
+ // ==========================================
3321
+ /**
3322
+ * Scan all loaded metadata for references pointing at the given
3323
+ * `{type, name}` target. Returns one row per referring artifact with
3324
+ * the path that produced the hit, so the admin UI can render an
3325
+ * "Used by" panel before destructive actions (rename / delete /
3326
+ * type-narrowing).
3327
+ *
3328
+ * Coverage is driven by the hand-curated {@link REFERENCE_PATHS}
3329
+ * registry. Types not present in the registry simply return no hits
3330
+ * — the engine never throws.
3331
+ */
3332
+ async findReferencesToMeta(request) {
3333
+ const singularTarget = import_shared2.PLURAL_TO_SINGULAR[request.type] ?? request.type;
3334
+ const targetName = request.name;
3335
+ const matchers = REFERENCE_PATHS[singularTarget];
3336
+ if (!matchers || matchers.length === 0) {
3337
+ return { references: [] };
3338
+ }
3339
+ const seen = /* @__PURE__ */ new Set();
3340
+ const out = [];
3341
+ await Promise.all(
3342
+ matchers.map(async (matcher) => {
3343
+ let items = [];
3344
+ try {
3345
+ const result = await this.getMetaItems({
3346
+ type: matcher.fromType,
3347
+ ...request.organizationId ? { organizationId: request.organizationId } : {}
3348
+ });
3349
+ items = result?.items ?? [];
3350
+ } catch {
3351
+ return;
3352
+ }
3353
+ for (const raw of items) {
3354
+ if (!raw || typeof raw !== "object") continue;
3355
+ const sourceName = raw.name;
3356
+ if (!sourceName) continue;
3357
+ const isSelfReference = matcher.fromType === singularTarget && sourceName === targetName;
3358
+ for (const path of matcher.paths) {
3359
+ const values = extractPathValues(raw, path);
3360
+ if (!values.includes(targetName)) continue;
3361
+ if (isSelfReference && !path.includes("[]") && !path.includes("{}")) continue;
3362
+ const key = `${matcher.fromType}|${sourceName}|${path}`;
3363
+ if (seen.has(key)) continue;
3364
+ seen.add(key);
3365
+ const label = raw.label;
3366
+ out.push({
3367
+ type: matcher.fromType,
3368
+ name: sourceName,
3369
+ ...label ? { label } : {},
3370
+ path,
3371
+ kind: matcher.kind
3372
+ });
3373
+ }
3374
+ }
3375
+ })
3376
+ );
3377
+ out.sort((a, b) => a.type.localeCompare(b.type) || a.name.localeCompare(b.name));
3378
+ return { references: out };
3379
+ }
3380
+ // ==========================================
2824
3381
  // Feed Operations
2825
3382
  // ==========================================
2826
3383
  async listFeed(request) {
@@ -2967,18 +3524,30 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
2967
3524
  for (const entry of import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY) {
2968
3525
  if (!entry.allowOrgOverride) continue;
2969
3526
  out.add(entry.type);
2970
- const plural = import_shared.SINGULAR_TO_PLURAL[entry.type];
3527
+ const plural = import_shared2.SINGULAR_TO_PLURAL[entry.type];
2971
3528
  if (plural) out.add(plural);
2972
3529
  }
2973
3530
  return out;
2974
3531
  })();
3532
+ /**
3533
+ * Phase 3a-env-writable: parse `OBJECTSTACK_METADATA_WRITABLE` once.
3534
+ * Comma-separated singular type names. When the env var is set, the
3535
+ * listed types get treated as `allowOrgOverride: true` regardless of
3536
+ * their static registry entry. This is the runtime escape hatch admins
3537
+ * use to enable Studio-side editing of types whose protocol-level flag
3538
+ * is still false (object, field, permission, …).
3539
+ *
3540
+ * Memoised at first call. Tests can override by clearing the cache via
3541
+ * {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
3542
+ */
3543
+ _ObjectStackProtocolImplementation._envWritableTypes = null;
2975
3544
  var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
2976
3545
 
2977
3546
  // src/engine.ts
2978
3547
  var import_kernel4 = require("@objectstack/spec/kernel");
2979
3548
  var import_core = require("@objectstack/core");
2980
- var import_system = require("@objectstack/spec/system");
2981
- var import_shared2 = require("@objectstack/spec/shared");
3549
+ var import_system2 = require("@objectstack/spec/system");
3550
+ var import_shared3 = require("@objectstack/spec/shared");
2982
3551
  var import_formula2 = require("@objectstack/formula");
2983
3552
 
2984
3553
  // src/hook-wrappers.ts
@@ -3780,7 +4349,7 @@ var _ObjectQL = class _ObjectQL {
3780
4349
  */
3781
4350
  getStatus() {
3782
4351
  return {
3783
- name: import_system.CoreServiceName.enum.data,
4352
+ name: import_system2.CoreServiceName.enum.data,
3784
4353
  status: "running",
3785
4354
  version: "0.9.0",
3786
4355
  features: ["crud", "query", "aggregate", "transactions", "metadata"]
@@ -4248,9 +4817,9 @@ var _ObjectQL = class _ObjectQL {
4248
4817
  const itemName = resolveMetadataItemName(key, item);
4249
4818
  if (itemName) {
4250
4819
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
4251
- this._registry.registerItem((0, import_shared2.pluralToSingular)(key), toRegister, "name", id);
4820
+ this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", id);
4252
4821
  } else {
4253
- this.logger.warn(`Skipping ${(0, import_shared2.pluralToSingular)(key)} without a derivable name`, { id });
4822
+ this.logger.warn(`Skipping ${(0, import_shared3.pluralToSingular)(key)} without a derivable name`, { id });
4254
4823
  }
4255
4824
  }
4256
4825
  }
@@ -4377,7 +4946,7 @@ var _ObjectQL = class _ObjectQL {
4377
4946
  const itemName = resolveMetadataItemName(key, item);
4378
4947
  if (itemName) {
4379
4948
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
4380
- this._registry.registerItem((0, import_shared2.pluralToSingular)(key), toRegister, "name", ownerId);
4949
+ this._registry.registerItem((0, import_shared3.pluralToSingular)(key), toRegister, "name", ownerId);
4381
4950
  }
4382
4951
  }
4383
4952
  }
@@ -4427,9 +4996,9 @@ var _ObjectQL = class _ObjectQL {
4427
4996
  resolveObjectName(name) {
4428
4997
  const schema = this._registry.getObject(name);
4429
4998
  if (schema) {
4430
- return import_system.StorageNameMapping.resolveTableName(schema);
4999
+ return import_system2.StorageNameMapping.resolveTableName(schema);
4431
5000
  }
4432
- return import_system.StorageNameMapping.resolveTableName({ name });
5001
+ return import_system2.StorageNameMapping.resolveTableName({ name });
4433
5002
  }
4434
5003
  /**
4435
5004
  * Helper to get the target driver
@@ -5199,7 +5768,7 @@ var _ObjectQL = class _ObjectQL {
5199
5768
  for (const obj of allObjects) {
5200
5769
  const driver = this.getDriverForObject(obj.name);
5201
5770
  if (!driver) continue;
5202
- const tableName = import_system.StorageNameMapping.resolveTableName(obj);
5771
+ const tableName = import_system2.StorageNameMapping.resolveTableName(obj);
5203
5772
  if (typeof driver.syncSchemasBatch === "function" && driver.supports?.batchSchemaSync) {
5204
5773
  }
5205
5774
  if (typeof driver.syncSchema === "function") {
@@ -5535,7 +6104,7 @@ var MetadataFacade = class {
5535
6104
  };
5536
6105
 
5537
6106
  // src/plugin.ts
5538
- var import_system2 = require("@objectstack/spec/system");
6107
+ var import_system3 = require("@objectstack/spec/system");
5539
6108
  function hasLoadMetaFromDb(service) {
5540
6109
  return typeof service === "object" && service !== null && typeof service["loadMetaFromDb"] === "function";
5541
6110
  }
@@ -5925,7 +6494,7 @@ var ObjectQLPlugin = class {
5925
6494
  skipped++;
5926
6495
  continue;
5927
6496
  }
5928
- const tableName = import_system2.StorageNameMapping.resolveTableName(obj);
6497
+ const tableName = import_system3.StorageNameMapping.resolveTableName(obj);
5929
6498
  let group = driverGroups.get(driver);
5930
6499
  if (!group) {
5931
6500
  group = [];