@objectstack/objectql 7.0.0 → 7.2.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.d.mts +361 -7
- package/dist/index.d.ts +361 -7
- package/dist/index.js +1077 -164
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1074 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -440,6 +440,14 @@ var SchemaRegistry = class {
|
|
|
440
440
|
if (collection.has(storageKey)) {
|
|
441
441
|
this.log(`[Registry] Overwriting ${type}: ${storageKey}`);
|
|
442
442
|
}
|
|
443
|
+
if (packageId && collection.has(baseName)) {
|
|
444
|
+
const dbOnly = collection.get(baseName);
|
|
445
|
+
if (dbOnly && !dbOnly._packageId) {
|
|
446
|
+
console.warn(
|
|
447
|
+
`[Registry] Collision: ${type}/${baseName} ships from package "${packageId}" but a runtime-authored row with the same name already exists in sys_metadata. The runtime row will shadow the package value (ADR-0005 overlay precedence). Rename one, or delete the sys_metadata row if the package value should win.`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
443
451
|
collection.set(storageKey, item);
|
|
444
452
|
this.log(`[Registry] Registered ${type}: ${storageKey}`);
|
|
445
453
|
}
|
|
@@ -496,7 +504,9 @@ var SchemaRegistry = class {
|
|
|
496
504
|
const direct = collection.get(name);
|
|
497
505
|
if (direct) return direct;
|
|
498
506
|
for (const [key, item] of collection) {
|
|
499
|
-
if (key.endsWith(`:${name}`))
|
|
507
|
+
if (key.endsWith(`:${name}`)) {
|
|
508
|
+
return item;
|
|
509
|
+
}
|
|
500
510
|
}
|
|
501
511
|
return void 0;
|
|
502
512
|
}
|
|
@@ -676,6 +686,12 @@ var import_shared = require("@objectstack/spec/shared");
|
|
|
676
686
|
var OVERLAY_ALLOWED_TYPES = new Set(
|
|
677
687
|
import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
|
|
678
688
|
);
|
|
689
|
+
var STATIC_REGISTRY_TYPES = new Set(
|
|
690
|
+
import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)
|
|
691
|
+
);
|
|
692
|
+
var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
|
|
693
|
+
import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type)
|
|
694
|
+
);
|
|
679
695
|
var _envWritableMetadataTypes = null;
|
|
680
696
|
function envWritableMetadataTypes() {
|
|
681
697
|
if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
|
|
@@ -725,11 +741,16 @@ var SysMetadataRepository = class {
|
|
|
725
741
|
/**
|
|
726
742
|
* Read the current overlay row. Returns null if no row exists —
|
|
727
743
|
* callers (e.g. LayeredRepository) fall through to lower layers.
|
|
744
|
+
*
|
|
745
|
+
* `opts.state` selects which lifecycle row to read: defaults to the
|
|
746
|
+
* live published row (`'active'`). Pass `'draft'` to read the pending
|
|
747
|
+
* unpublished revision (if any).
|
|
728
748
|
*/
|
|
729
|
-
async get(ref) {
|
|
749
|
+
async get(ref, opts) {
|
|
730
750
|
this.assertOpen();
|
|
751
|
+
const state = opts?.state ?? "active";
|
|
731
752
|
const row = await this.engine.findOne("sys_metadata", {
|
|
732
|
-
where: this.whereFor(ref)
|
|
753
|
+
where: this.whereFor(ref, state)
|
|
733
754
|
});
|
|
734
755
|
if (!row) return null;
|
|
735
756
|
return this.rowToItem(ref, row);
|
|
@@ -772,12 +793,13 @@ var SysMetadataRepository = class {
|
|
|
772
793
|
}
|
|
773
794
|
async put(ref, spec, opts) {
|
|
774
795
|
this.assertOpen();
|
|
775
|
-
this.assertAllowed(ref.type);
|
|
796
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
797
|
+
const state = opts.state ?? "active";
|
|
776
798
|
const body = spec ?? {};
|
|
777
799
|
const hash = (0, import_metadata_core.hashSpec)(body);
|
|
778
800
|
const result = await this.withTxn(async (ctx) => {
|
|
779
801
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
780
|
-
where: this.whereFor(ref),
|
|
802
|
+
where: this.whereFor(ref, state),
|
|
781
803
|
context: ctx
|
|
782
804
|
});
|
|
783
805
|
const existingHash = existing?.checksum ?? null;
|
|
@@ -789,7 +811,8 @@ var SysMetadataRepository = class {
|
|
|
789
811
|
return { skipped: true, version: hash, seq: item2.seq, item: item2 };
|
|
790
812
|
}
|
|
791
813
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
792
|
-
const
|
|
814
|
+
const baseOp = existing ? "update" : "create";
|
|
815
|
+
const op = opts.opType ?? baseOp;
|
|
793
816
|
const version = await this.nextItemVersion(ref, ctx);
|
|
794
817
|
const eventSeq = await this.nextEventSeq(ctx);
|
|
795
818
|
const parentRowData = {
|
|
@@ -798,7 +821,7 @@ var SysMetadataRepository = class {
|
|
|
798
821
|
organization_id: this.organizationId,
|
|
799
822
|
metadata: JSON.stringify(body),
|
|
800
823
|
checksum: hash,
|
|
801
|
-
state
|
|
824
|
+
state,
|
|
802
825
|
version,
|
|
803
826
|
updated_at: now
|
|
804
827
|
};
|
|
@@ -864,25 +887,28 @@ var SysMetadataRepository = class {
|
|
|
864
887
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
865
888
|
}
|
|
866
889
|
this.seqCounter = result.seq;
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
890
|
+
if (state === "active") {
|
|
891
|
+
this.broadcast({
|
|
892
|
+
seq: result.seq,
|
|
893
|
+
op: result.op,
|
|
894
|
+
ref: this.fullRef(ref),
|
|
895
|
+
hash: result.version,
|
|
896
|
+
parentHash: result.existingHash,
|
|
897
|
+
actor: result.actor,
|
|
898
|
+
message: result.message,
|
|
899
|
+
ts: result.now,
|
|
900
|
+
source: result.source
|
|
901
|
+
});
|
|
902
|
+
}
|
|
878
903
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
879
904
|
}
|
|
880
905
|
async delete(ref, opts) {
|
|
881
906
|
this.assertOpen();
|
|
882
|
-
this.assertAllowed(ref.type);
|
|
907
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
908
|
+
const state = opts.state ?? "active";
|
|
883
909
|
const result = await this.withTxn(async (ctx) => {
|
|
884
910
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
885
|
-
where: this.whereFor(ref),
|
|
911
|
+
where: this.whereFor(ref, state),
|
|
886
912
|
context: ctx
|
|
887
913
|
});
|
|
888
914
|
if (!existing) {
|
|
@@ -899,32 +925,38 @@ var SysMetadataRepository = class {
|
|
|
899
925
|
);
|
|
900
926
|
}
|
|
901
927
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
902
|
-
|
|
903
|
-
|
|
928
|
+
let version = 0;
|
|
929
|
+
let eventSeq = 0;
|
|
930
|
+
if (state === "active") {
|
|
931
|
+
version = await this.nextItemVersion(ref, ctx);
|
|
932
|
+
eventSeq = await this.nextEventSeq(ctx);
|
|
933
|
+
}
|
|
904
934
|
await this.engine.delete("sys_metadata", {
|
|
905
935
|
where: { id: existingId },
|
|
906
936
|
context: ctx
|
|
907
937
|
});
|
|
908
|
-
|
|
909
|
-
this.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
938
|
+
if (state === "active") {
|
|
939
|
+
await this.engine.insert(
|
|
940
|
+
this.historyTable,
|
|
941
|
+
{
|
|
942
|
+
id: this.uuid(),
|
|
943
|
+
event_seq: eventSeq,
|
|
944
|
+
type: ref.type,
|
|
945
|
+
name: ref.name,
|
|
946
|
+
version,
|
|
947
|
+
operation_type: "delete",
|
|
948
|
+
metadata: null,
|
|
949
|
+
checksum: null,
|
|
950
|
+
previous_checksum: existingHash,
|
|
951
|
+
change_note: opts.message,
|
|
952
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
953
|
+
organization_id: this.organizationId,
|
|
954
|
+
recorded_by: opts.actor,
|
|
955
|
+
recorded_at: now
|
|
956
|
+
},
|
|
957
|
+
{ context: ctx }
|
|
958
|
+
);
|
|
959
|
+
}
|
|
928
960
|
return {
|
|
929
961
|
eventSeq,
|
|
930
962
|
existingHash,
|
|
@@ -934,20 +966,117 @@ var SysMetadataRepository = class {
|
|
|
934
966
|
actor: opts.actor
|
|
935
967
|
};
|
|
936
968
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
969
|
+
if (state === "active") {
|
|
970
|
+
this.seqCounter = result.eventSeq;
|
|
971
|
+
this.broadcast({
|
|
972
|
+
seq: result.eventSeq,
|
|
973
|
+
op: "delete",
|
|
974
|
+
ref: this.fullRef(ref),
|
|
975
|
+
hash: null,
|
|
976
|
+
parentHash: result.existingHash,
|
|
977
|
+
actor: result.actor,
|
|
978
|
+
message: result.message,
|
|
979
|
+
ts: result.now,
|
|
980
|
+
source: result.source
|
|
981
|
+
});
|
|
982
|
+
}
|
|
949
983
|
return { seq: result.eventSeq };
|
|
950
984
|
}
|
|
985
|
+
/**
|
|
986
|
+
* Promote the pending draft row for `ref` into the live (`active`)
|
|
987
|
+
* overlay. Atomic: reads the draft inside the same transaction, runs
|
|
988
|
+
* the canonical `put` to upsert the active row (which appends a
|
|
989
|
+
* history event with `operation_type='publish'`), then deletes the
|
|
990
|
+
* draft row.
|
|
991
|
+
*
|
|
992
|
+
* Errors if no draft exists (callers should 404). The active row's
|
|
993
|
+
* `parentVersion` is computed from the current active hash so this
|
|
994
|
+
* also surfaces optimistic-lock conflicts when something else has
|
|
995
|
+
* published in between (e.g. another admin reverted to an older
|
|
996
|
+
* version since the draft was authored).
|
|
997
|
+
*/
|
|
998
|
+
async promoteDraft(ref, opts) {
|
|
999
|
+
this.assertOpen();
|
|
1000
|
+
const draft = await this.get(ref, { state: "draft" });
|
|
1001
|
+
if (!draft) {
|
|
1002
|
+
const err = new Error(
|
|
1003
|
+
`[no_draft] No pending draft exists for ${ref.type}/${ref.name} \u2014 nothing to publish.`
|
|
1004
|
+
);
|
|
1005
|
+
err.code = "no_draft";
|
|
1006
|
+
err.status = 404;
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
1010
|
+
const result = await this.put(ref, draft.body, {
|
|
1011
|
+
parentVersion: currentActive?.hash ?? null,
|
|
1012
|
+
actor: opts.actor,
|
|
1013
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
1014
|
+
message: opts.message ?? `publish draft (hash ${draft.hash})`,
|
|
1015
|
+
intent: opts.intent ?? "override-artifact",
|
|
1016
|
+
state: "active",
|
|
1017
|
+
opType: "publish"
|
|
1018
|
+
});
|
|
1019
|
+
try {
|
|
1020
|
+
await this.delete(ref, {
|
|
1021
|
+
parentVersion: draft.hash,
|
|
1022
|
+
actor: opts.actor,
|
|
1023
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
1024
|
+
intent: opts.intent ?? "override-artifact",
|
|
1025
|
+
state: "draft"
|
|
1026
|
+
});
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Restore the body recorded in history at `targetVersion` (per-org
|
|
1033
|
+
* lineage counter) as the new active row. Writes a history event
|
|
1034
|
+
* with `operation_type='revert'` so the audit trail captures the
|
|
1035
|
+
* intent. Does NOT touch any draft row.
|
|
1036
|
+
*
|
|
1037
|
+
* Throws `[version_not_found]` (404) if the target version row is
|
|
1038
|
+
* missing or is a delete tombstone (no body to restore).
|
|
1039
|
+
*/
|
|
1040
|
+
async restoreVersion(ref, targetVersion, opts) {
|
|
1041
|
+
this.assertOpen();
|
|
1042
|
+
const full = this.fullRef(ref);
|
|
1043
|
+
const row = await this.engine.findOne(this.historyTable, {
|
|
1044
|
+
where: {
|
|
1045
|
+
organization_id: this.organizationId,
|
|
1046
|
+
type: full.type,
|
|
1047
|
+
name: full.name,
|
|
1048
|
+
version: targetVersion
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
if (!row) {
|
|
1052
|
+
const err = new Error(
|
|
1053
|
+
`[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`
|
|
1054
|
+
);
|
|
1055
|
+
err.code = "version_not_found";
|
|
1056
|
+
err.status = 404;
|
|
1057
|
+
throw err;
|
|
1058
|
+
}
|
|
1059
|
+
const raw = row.metadata;
|
|
1060
|
+
if (raw === null || raw === void 0) {
|
|
1061
|
+
const err = new Error(
|
|
1062
|
+
`[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone \u2014 nothing to restore.`
|
|
1063
|
+
);
|
|
1064
|
+
err.code = "version_not_restorable";
|
|
1065
|
+
err.status = 409;
|
|
1066
|
+
throw err;
|
|
1067
|
+
}
|
|
1068
|
+
const body = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1069
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
1070
|
+
return this.put(ref, body, {
|
|
1071
|
+
parentVersion: currentActive?.hash ?? null,
|
|
1072
|
+
actor: opts.actor,
|
|
1073
|
+
source: opts.source ?? "sys-metadata-repo.revert",
|
|
1074
|
+
message: opts.message ?? `revert to version ${targetVersion}`,
|
|
1075
|
+
intent: opts.intent ?? "override-artifact",
|
|
1076
|
+
state: "active",
|
|
1077
|
+
opType: "revert"
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
951
1080
|
async *list(filter) {
|
|
952
1081
|
this.assertOpen();
|
|
953
1082
|
const where = {
|
|
@@ -1002,6 +1131,7 @@ var SysMetadataRepository = class {
|
|
|
1002
1131
|
ref: full,
|
|
1003
1132
|
hash: row.checksum ?? null,
|
|
1004
1133
|
parentHash: row.previous_checksum ?? null,
|
|
1134
|
+
version: typeof row.version === "number" ? row.version : void 0,
|
|
1005
1135
|
actor: row.recorded_by ?? "unknown",
|
|
1006
1136
|
message: row.change_note ?? void 0,
|
|
1007
1137
|
ts: row.recorded_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
@@ -1083,29 +1213,52 @@ var SysMetadataRepository = class {
|
|
|
1083
1213
|
assertOpen() {
|
|
1084
1214
|
if (this.closed) throw new Error("SysMetadataRepository is closed");
|
|
1085
1215
|
}
|
|
1086
|
-
|
|
1216
|
+
/**
|
|
1217
|
+
* Defense-in-depth authorization gate.
|
|
1218
|
+
*
|
|
1219
|
+
* `intent` defaults to `'override-artifact'` (the historical strict
|
|
1220
|
+
* behavior). The protocol layer passes `'runtime-only'` after it has
|
|
1221
|
+
* verified — via the schema registry — that no artifact item exists
|
|
1222
|
+
* at `(type, name)`. In that case we accept types with
|
|
1223
|
+
* `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
|
|
1224
|
+
*
|
|
1225
|
+
* The env-var escape hatch (`OBJECTSTACK_METADATA_WRITABLE`) still
|
|
1226
|
+
* applies to BOTH intents, so operators can opt into artifact
|
|
1227
|
+
* overrides at runtime for emergency fixes.
|
|
1228
|
+
*/
|
|
1229
|
+
assertAllowed(type, intent = "override-artifact") {
|
|
1087
1230
|
const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
|
|
1088
1231
|
const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
|
|
1089
1232
|
if (allowedByRegistry) return;
|
|
1233
|
+
if (intent === "runtime-only") {
|
|
1234
|
+
if (RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if (!STATIC_REGISTRY_TYPES.has(singular) && !STATIC_REGISTRY_TYPES.has(type)) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1090
1241
|
const env = envWritableMetadataTypes();
|
|
1091
1242
|
if (env.has(singular) || env.has(type)) return;
|
|
1092
1243
|
const allowed = [
|
|
1093
1244
|
...OVERLAY_ALLOWED_TYPES,
|
|
1094
1245
|
...envWritableMetadataTypes()
|
|
1095
1246
|
];
|
|
1247
|
+
const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
|
|
1248
|
+
const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
|
|
1096
1249
|
const err = new Error(
|
|
1097
|
-
`[
|
|
1250
|
+
`[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
|
|
1098
1251
|
);
|
|
1099
|
-
err.code =
|
|
1252
|
+
err.code = code;
|
|
1100
1253
|
err.status = 403;
|
|
1101
1254
|
throw err;
|
|
1102
1255
|
}
|
|
1103
|
-
whereFor(ref) {
|
|
1256
|
+
whereFor(ref, state = "active") {
|
|
1104
1257
|
return {
|
|
1105
1258
|
type: ref.type,
|
|
1106
1259
|
name: ref.name,
|
|
1107
1260
|
organization_id: this.organizationId,
|
|
1108
|
-
state
|
|
1261
|
+
state
|
|
1109
1262
|
};
|
|
1110
1263
|
}
|
|
1111
1264
|
fullRef(ref) {
|
|
@@ -1202,36 +1355,58 @@ var SysMetadataRepository = class {
|
|
|
1202
1355
|
// src/protocol.ts
|
|
1203
1356
|
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
1204
1357
|
var import_data2 = require("@objectstack/spec/data");
|
|
1205
|
-
var
|
|
1206
|
-
var import_ui2 = require("@objectstack/spec/ui");
|
|
1207
|
-
var import_identity = require("@objectstack/spec/identity");
|
|
1208
|
-
var import_security = require("@objectstack/spec/security");
|
|
1358
|
+
var import_shared3 = require("@objectstack/spec/shared");
|
|
1209
1359
|
var import_system = require("@objectstack/spec/system");
|
|
1210
|
-
var
|
|
1211
|
-
var
|
|
1212
|
-
var import_kernel3 = require("@objectstack/spec/kernel");
|
|
1360
|
+
var import_kernel4 = require("@objectstack/spec/kernel");
|
|
1361
|
+
var import_kernel5 = require("@objectstack/spec/kernel");
|
|
1213
1362
|
var import_zod = require("zod");
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1363
|
+
|
|
1364
|
+
// src/metadata-diagnostics.ts
|
|
1365
|
+
var import_kernel3 = require("@objectstack/spec/kernel");
|
|
1366
|
+
var import_shared2 = require("@objectstack/spec/shared");
|
|
1367
|
+
function computeMetadataDiagnostics(type, item) {
|
|
1368
|
+
const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
|
|
1369
|
+
const schema = (0, import_kernel3.getMetadataTypeSchema)(singular);
|
|
1370
|
+
if (!schema) return void 0;
|
|
1371
|
+
if (item === null || item === void 0 || typeof item !== "object") {
|
|
1372
|
+
return {
|
|
1373
|
+
valid: false,
|
|
1374
|
+
errors: [{
|
|
1375
|
+
path: "",
|
|
1376
|
+
message: "Metadata document must be a non-null object",
|
|
1377
|
+
code: "invalid_type"
|
|
1378
|
+
}]
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
const candidate = "_diagnostics" in item ? stripDiagnostics(item) : item;
|
|
1382
|
+
const parsed = schema.safeParse(candidate);
|
|
1383
|
+
if (parsed.success) {
|
|
1384
|
+
return { valid: true };
|
|
1385
|
+
}
|
|
1386
|
+
const errors = parsed.error.issues.map((issue) => ({
|
|
1387
|
+
path: issue.path.map(String).join("."),
|
|
1388
|
+
message: issue.message,
|
|
1389
|
+
code: issue.code
|
|
1390
|
+
}));
|
|
1391
|
+
return { valid: false, errors };
|
|
1392
|
+
}
|
|
1393
|
+
function stripDiagnostics(item) {
|
|
1394
|
+
const { _diagnostics: _drop, ...rest } = item;
|
|
1395
|
+
void _drop;
|
|
1396
|
+
return rest;
|
|
1397
|
+
}
|
|
1398
|
+
function decorateMetadataItem(type, item) {
|
|
1399
|
+
if (!item || typeof item !== "object") return item;
|
|
1400
|
+
const diagnostics = computeMetadataDiagnostics(type, item);
|
|
1401
|
+
if (!diagnostics) return item;
|
|
1402
|
+
return { ...item, _diagnostics: diagnostics };
|
|
1403
|
+
}
|
|
1404
|
+
function decorateMetadataItems(type, items) {
|
|
1405
|
+
if (!Array.isArray(items)) return items;
|
|
1406
|
+
return items.map((item) => decorateMetadataItem(type, item));
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/protocol.ts
|
|
1235
1410
|
var TYPE_TO_FORM = import_system.METADATA_FORM_REGISTRY;
|
|
1236
1411
|
var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
1237
1412
|
function toJsonSchemaSafe(schema) {
|
|
@@ -1261,9 +1436,17 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1261
1436
|
abstract: { type: "boolean", default: false },
|
|
1262
1437
|
datasource: { type: "string" },
|
|
1263
1438
|
fields: {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1439
|
+
// Canonical Object.fields is a name-keyed map
|
|
1440
|
+
// (Record<string, FieldDefinition>) — insertion order is
|
|
1441
|
+
// display order. The SchemaForm engine recognises
|
|
1442
|
+
// `additionalProperties` as a Record and dispatches to
|
|
1443
|
+
// the `record` form-field renderer (ADR-0007). The form
|
|
1444
|
+
// layout in `object.form.ts` declares `type: 'record'`
|
|
1445
|
+
// so the inner `additionalProperties` schema is used to
|
|
1446
|
+
// shape each value.
|
|
1447
|
+
type: "object",
|
|
1448
|
+
default: {},
|
|
1449
|
+
additionalProperties: {
|
|
1267
1450
|
type: "object",
|
|
1268
1451
|
properties: {
|
|
1269
1452
|
name: { type: "string" },
|
|
@@ -1274,7 +1457,7 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1274
1457
|
defaultValue: {},
|
|
1275
1458
|
description: { type: "string" }
|
|
1276
1459
|
},
|
|
1277
|
-
required: ["
|
|
1460
|
+
required: ["type"]
|
|
1278
1461
|
}
|
|
1279
1462
|
},
|
|
1280
1463
|
capabilities: { type: "object", additionalProperties: true }
|
|
@@ -1419,19 +1602,22 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1419
1602
|
additionalProperties: true
|
|
1420
1603
|
}
|
|
1421
1604
|
};
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1605
|
+
function resolveOverlaySchema(type, _item) {
|
|
1606
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
1607
|
+
return (0, import_kernel4.getMetadataTypeSchema)(singular) ?? null;
|
|
1608
|
+
}
|
|
1609
|
+
function mergeArtifactProtection(item, artifactItem) {
|
|
1610
|
+
if (item === void 0 || item === null) return item;
|
|
1611
|
+
if (artifactItem === void 0 || artifactItem === null) return item;
|
|
1612
|
+
const a = artifactItem;
|
|
1613
|
+
if (typeof a !== "object") return item;
|
|
1614
|
+
const out = { ...item };
|
|
1615
|
+
if (a._lock !== void 0) out._lock = a._lock;
|
|
1616
|
+
if (a._lockReason !== void 0) out._lockReason = a._lockReason;
|
|
1617
|
+
if (a._packageId !== void 0) out._packageId = a._packageId;
|
|
1618
|
+
if (a._packageVersion !== void 0) out._packageVersion = a._packageVersion;
|
|
1619
|
+
if (a._provenance !== void 0) out._provenance = a._provenance;
|
|
1620
|
+
return out;
|
|
1435
1621
|
}
|
|
1436
1622
|
function simpleHash(str) {
|
|
1437
1623
|
let hash = 0;
|
|
@@ -1564,6 +1750,32 @@ function extractPathValues(item, path) {
|
|
|
1564
1750
|
}
|
|
1565
1751
|
return out;
|
|
1566
1752
|
}
|
|
1753
|
+
function diffShallow(from, to) {
|
|
1754
|
+
const added = [];
|
|
1755
|
+
const removed = [];
|
|
1756
|
+
const changed = [];
|
|
1757
|
+
const fromKeys = new Set(Object.keys(from ?? {}));
|
|
1758
|
+
const toKeys = new Set(Object.keys(to ?? {}));
|
|
1759
|
+
for (const k of toKeys) {
|
|
1760
|
+
if (!fromKeys.has(k)) {
|
|
1761
|
+
added.push({ path: k, value: to[k] });
|
|
1762
|
+
} else {
|
|
1763
|
+
const a = from[k];
|
|
1764
|
+
const b = to[k];
|
|
1765
|
+
const aStr = JSON.stringify(a);
|
|
1766
|
+
const bStr = JSON.stringify(b);
|
|
1767
|
+
if (aStr !== bStr) {
|
|
1768
|
+
changed.push({ path: k, from: a, to: b });
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
for (const k of fromKeys) {
|
|
1773
|
+
if (!toKeys.has(k)) {
|
|
1774
|
+
removed.push({ path: k, value: from[k] });
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return { added, removed, changed };
|
|
1778
|
+
}
|
|
1567
1779
|
function detectDestructiveObjectChanges(prev, next) {
|
|
1568
1780
|
if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
|
|
1569
1781
|
const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
|
|
@@ -1695,6 +1907,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1695
1907
|
}
|
|
1696
1908
|
}
|
|
1697
1909
|
}
|
|
1910
|
+
const draftPartialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id) WHERE state = 'draft'";
|
|
1911
|
+
try {
|
|
1912
|
+
await exec(draftPartialSql);
|
|
1913
|
+
} catch (err) {
|
|
1914
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1915
|
+
if (/partial|where clause|syntax/i.test(msg)) {
|
|
1916
|
+
try {
|
|
1917
|
+
await exec(
|
|
1918
|
+
"CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id)"
|
|
1919
|
+
);
|
|
1920
|
+
} catch {
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1698
1924
|
} catch {
|
|
1699
1925
|
}
|
|
1700
1926
|
}
|
|
@@ -1815,11 +2041,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1815
2041
|
const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
|
|
1816
2042
|
const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
|
|
1817
2043
|
const registryByType = new Map(
|
|
1818
|
-
|
|
2044
|
+
import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
|
|
1819
2045
|
);
|
|
1820
2046
|
const entries = allTypes.map((type) => {
|
|
1821
|
-
const singular =
|
|
1822
|
-
const zodSchema =
|
|
2047
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
2048
|
+
const zodSchema = (0, import_kernel4.getMetadataTypeSchema)(singular);
|
|
1823
2049
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
1824
2050
|
const form = TYPE_TO_FORM[singular];
|
|
1825
2051
|
const base = registryByType.get(singular);
|
|
@@ -1860,19 +2086,82 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1860
2086
|
});
|
|
1861
2087
|
return { types: allTypes, entries };
|
|
1862
2088
|
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Sweep all (or filtered) metadata types and report entries that
|
|
2091
|
+
* fail spec validation. Powers the Studio governance view
|
|
2092
|
+
* (`GET /api/v1/meta/diagnostics`) and `os doctor`-style CLI
|
|
2093
|
+
* checks.
|
|
2094
|
+
*
|
|
2095
|
+
* `severity` defaults to `'error'` — only entries with at least
|
|
2096
|
+
* one Zod error issue are returned. `'warning'` includes
|
|
2097
|
+
* everything we surface (warnings are reserved for a future lint
|
|
2098
|
+
* layer on top of spec validation).
|
|
2099
|
+
*
|
|
2100
|
+
* `type` may be either a singular (`'view'`) or plural (`'views'`)
|
|
2101
|
+
* identifier; the underlying `getMetaItems` already normalises.
|
|
2102
|
+
*
|
|
2103
|
+
* Implementation note: leverages the `_diagnostics` already
|
|
2104
|
+
* decorated onto items by `getMetaItems()` to avoid running
|
|
2105
|
+
* `safeParse()` twice. For types whose schema is unregistered we
|
|
2106
|
+
* skip silently (they cannot be validated and should not appear
|
|
2107
|
+
* as "valid" either — they are simply opaque to this report).
|
|
2108
|
+
*/
|
|
2109
|
+
async getMetaDiagnostics(request = {}) {
|
|
2110
|
+
const includeWarnings = request.severity === "warning";
|
|
2111
|
+
const targetTypes = request.type ? [request.type] : import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => (0, import_kernel4.getMetadataTypeSchema)(e.type)).map((e) => e.type);
|
|
2112
|
+
const entries = [];
|
|
2113
|
+
const stats = {};
|
|
2114
|
+
let scannedItems = 0;
|
|
2115
|
+
for (const t of targetTypes) {
|
|
2116
|
+
let listed;
|
|
2117
|
+
try {
|
|
2118
|
+
listed = await this.getMetaItems({
|
|
2119
|
+
type: t,
|
|
2120
|
+
organizationId: request.organizationId,
|
|
2121
|
+
packageId: request.packageId
|
|
2122
|
+
});
|
|
2123
|
+
} catch {
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
|
|
2127
|
+
const pkgSet = /* @__PURE__ */ new Set();
|
|
2128
|
+
for (const item of items) {
|
|
2129
|
+
scannedItems += 1;
|
|
2130
|
+
const pkg = item?._packageId ?? null;
|
|
2131
|
+
if (pkg) pkgSet.add(pkg);
|
|
2132
|
+
const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
|
|
2133
|
+
if (!diag) continue;
|
|
2134
|
+
if (diag.valid && !includeWarnings) continue;
|
|
2135
|
+
if (diag.valid && includeWarnings && !diag.warnings?.length) continue;
|
|
2136
|
+
entries.push({
|
|
2137
|
+
type: t,
|
|
2138
|
+
name: typeof item?.name === "string" ? item.name : "<unknown>",
|
|
2139
|
+
diagnostics: diag
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
stats[t] = { count: items.length, packages: [...pkgSet].sort() };
|
|
2143
|
+
}
|
|
2144
|
+
return {
|
|
2145
|
+
entries,
|
|
2146
|
+
total: entries.length,
|
|
2147
|
+
scannedTypes: targetTypes.length,
|
|
2148
|
+
scannedItems,
|
|
2149
|
+
stats
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
1863
2152
|
async getMetaItems(request) {
|
|
1864
2153
|
const { packageId } = request;
|
|
1865
2154
|
let items = [];
|
|
1866
2155
|
if (this.environmentId === void 0) {
|
|
1867
2156
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1868
2157
|
if (items.length === 0) {
|
|
1869
|
-
const alt =
|
|
2158
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1870
2159
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1871
2160
|
}
|
|
1872
2161
|
} else {
|
|
1873
2162
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1874
2163
|
if (items.length === 0) {
|
|
1875
|
-
const alt =
|
|
2164
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1876
2165
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1877
2166
|
}
|
|
1878
2167
|
}
|
|
@@ -1887,7 +2176,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1887
2176
|
if (packageId) whereClause._packageId = packageId;
|
|
1888
2177
|
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
1889
2178
|
if (!rs || rs.length === 0) {
|
|
1890
|
-
const alt =
|
|
2179
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1891
2180
|
if (alt) {
|
|
1892
2181
|
const altWhere = { type: alt, state: "active", organization_id: oid };
|
|
1893
2182
|
if (packageId) altWhere._packageId = packageId;
|
|
@@ -1954,28 +2243,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1954
2243
|
}
|
|
1955
2244
|
return {
|
|
1956
2245
|
type: request.type,
|
|
1957
|
-
items
|
|
2246
|
+
items: decorateMetadataItems(
|
|
2247
|
+
request.type,
|
|
2248
|
+
items.map((it) => {
|
|
2249
|
+
const a = this.lookupArtifactItem(
|
|
2250
|
+
request.type,
|
|
2251
|
+
it?.name
|
|
2252
|
+
);
|
|
2253
|
+
return mergeArtifactProtection(it, a);
|
|
2254
|
+
})
|
|
2255
|
+
)
|
|
1958
2256
|
};
|
|
1959
2257
|
}
|
|
1960
2258
|
async getMetaItem(request) {
|
|
1961
2259
|
let item;
|
|
1962
2260
|
const orgId = request.organizationId;
|
|
2261
|
+
const readState = request.state === "draft" ? "draft" : "active";
|
|
1963
2262
|
try {
|
|
1964
2263
|
const findOverlay = async (oid) => {
|
|
1965
2264
|
const where = {
|
|
1966
2265
|
type: request.type,
|
|
1967
2266
|
name: request.name,
|
|
1968
|
-
state:
|
|
2267
|
+
state: readState,
|
|
1969
2268
|
organization_id: oid
|
|
1970
2269
|
};
|
|
1971
2270
|
const rec = await this.engine.findOne("sys_metadata", { where });
|
|
1972
2271
|
if (rec) return rec;
|
|
1973
|
-
const alt =
|
|
2272
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1974
2273
|
if (alt) {
|
|
1975
2274
|
const altWhere = {
|
|
1976
2275
|
type: alt,
|
|
1977
2276
|
name: request.name,
|
|
1978
|
-
state:
|
|
2277
|
+
state: readState,
|
|
1979
2278
|
organization_id: oid
|
|
1980
2279
|
};
|
|
1981
2280
|
return await this.engine.findOne("sys_metadata", { where: altWhere });
|
|
@@ -1988,6 +2287,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1988
2287
|
}
|
|
1989
2288
|
} catch {
|
|
1990
2289
|
}
|
|
2290
|
+
if (readState === "draft") {
|
|
2291
|
+
if (item === void 0) {
|
|
2292
|
+
const err = new Error(
|
|
2293
|
+
`[no_draft] No pending draft exists for ${request.type}/${request.name}.`
|
|
2294
|
+
);
|
|
2295
|
+
err.code = "no_draft";
|
|
2296
|
+
err.status = 404;
|
|
2297
|
+
throw err;
|
|
2298
|
+
}
|
|
2299
|
+
return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, item) };
|
|
2300
|
+
}
|
|
1991
2301
|
if (item === void 0) {
|
|
1992
2302
|
try {
|
|
1993
2303
|
const services = this.getServicesRegistry?.();
|
|
@@ -1997,7 +2307,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1997
2307
|
if (fromService !== void 0 && fromService !== null) {
|
|
1998
2308
|
item = fromService;
|
|
1999
2309
|
} else {
|
|
2000
|
-
const alt =
|
|
2310
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2001
2311
|
if (alt) {
|
|
2002
2312
|
const altFromService = await metadataService.get(alt, request.name);
|
|
2003
2313
|
if (altFromService !== void 0 && altFromService !== null) {
|
|
@@ -2012,14 +2322,30 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2012
2322
|
if (item === void 0) {
|
|
2013
2323
|
item = this.engine.registry.getItem(request.type, request.name);
|
|
2014
2324
|
if (item === void 0) {
|
|
2015
|
-
const alt =
|
|
2325
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2016
2326
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
2017
2327
|
}
|
|
2018
2328
|
}
|
|
2329
|
+
const artifactItem = this.lookupArtifactItem(request.type, request.name);
|
|
2330
|
+
const decorated = decorateMetadataItem(
|
|
2331
|
+
request.type,
|
|
2332
|
+
mergeArtifactProtection(item, artifactItem)
|
|
2333
|
+
);
|
|
2334
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
2335
|
+
const lockState = (0, import_kernel5.resolveLockState)(decorated, artifactBacked);
|
|
2019
2336
|
return {
|
|
2020
2337
|
type: request.type,
|
|
2021
2338
|
name: request.name,
|
|
2022
|
-
item
|
|
2339
|
+
item: decorated,
|
|
2340
|
+
lock: lockState.lock,
|
|
2341
|
+
...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
|
|
2342
|
+
...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
|
|
2343
|
+
...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
|
|
2344
|
+
...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
|
|
2345
|
+
...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
|
|
2346
|
+
editable: lockState.editable,
|
|
2347
|
+
deletable: lockState.deletable,
|
|
2348
|
+
resettable: lockState.resettable
|
|
2023
2349
|
};
|
|
2024
2350
|
}
|
|
2025
2351
|
/**
|
|
@@ -2045,7 +2371,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2045
2371
|
if (metadataService && typeof metadataService.get === "function") {
|
|
2046
2372
|
let fromService = await metadataService.get(request.type, request.name);
|
|
2047
2373
|
if (fromService === void 0 || fromService === null) {
|
|
2048
|
-
const alt =
|
|
2374
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2049
2375
|
if (alt) fromService = await metadataService.get(alt, request.name);
|
|
2050
2376
|
}
|
|
2051
2377
|
if (fromService !== void 0 && fromService !== null) code = fromService;
|
|
@@ -2055,7 +2381,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2055
2381
|
if (code === null) {
|
|
2056
2382
|
let regItem = this.engine.registry.getItem(request.type, request.name);
|
|
2057
2383
|
if (regItem === void 0) {
|
|
2058
|
-
const alt =
|
|
2384
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2059
2385
|
if (alt) regItem = this.engine.registry.getItem(alt, request.name);
|
|
2060
2386
|
}
|
|
2061
2387
|
if (regItem !== void 0) code = regItem;
|
|
@@ -2072,7 +2398,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2072
2398
|
};
|
|
2073
2399
|
let rec = await this.engine.findOne("sys_metadata", { where });
|
|
2074
2400
|
if (!rec) {
|
|
2075
|
-
const alt =
|
|
2401
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2076
2402
|
if (alt) {
|
|
2077
2403
|
rec = await this.engine.findOne("sys_metadata", {
|
|
2078
2404
|
where: { ...where, type: alt }
|
|
@@ -2098,15 +2424,80 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2098
2424
|
} catch {
|
|
2099
2425
|
}
|
|
2100
2426
|
const effective = overlay ?? code;
|
|
2427
|
+
const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
|
|
2428
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
2429
|
+
const lockSource = code ?? overlay ?? {};
|
|
2430
|
+
const lockState = (0, import_kernel5.resolveLockState)(lockSource, artifactBacked);
|
|
2101
2431
|
return {
|
|
2102
2432
|
type: request.type,
|
|
2103
2433
|
name: request.name,
|
|
2104
2434
|
code,
|
|
2105
2435
|
overlay,
|
|
2106
2436
|
overlayScope,
|
|
2107
|
-
effective
|
|
2437
|
+
effective,
|
|
2438
|
+
..._diagnostics ? { _diagnostics } : {},
|
|
2439
|
+
lock: lockState.lock,
|
|
2440
|
+
...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
|
|
2441
|
+
...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
|
|
2442
|
+
...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
|
|
2443
|
+
...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
|
|
2444
|
+
...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
|
|
2445
|
+
editable: lockState.editable,
|
|
2446
|
+
deletable: lockState.deletable,
|
|
2447
|
+
resettable: lockState.resettable
|
|
2108
2448
|
};
|
|
2109
2449
|
}
|
|
2450
|
+
/**
|
|
2451
|
+
* ADR-0010 §3.6 / Phase 4.1 — read the metadata-protection audit log
|
|
2452
|
+
* for a single item. Returns the most-recent rows of
|
|
2453
|
+
* `sys_metadata_audit` for this (type, name) tuple, sorted newest
|
|
2454
|
+
* first. Refused (`denied`) and forced (`forced`) writes both appear
|
|
2455
|
+
* here — they never reach the `history` endpoint, which only tracks
|
|
2456
|
+
* successful body snapshots.
|
|
2457
|
+
*
|
|
2458
|
+
* The table is provisioned by `platform-objects` and is the
|
|
2459
|
+
* compliance surface for the lock-enforcement story. When the
|
|
2460
|
+
* environment has not yet provisioned the table (legacy install
|
|
2461
|
+
* prior to ADR-0010) the call returns `{ events: [] }` instead of
|
|
2462
|
+
* raising, keeping the Studio tab harmless.
|
|
2463
|
+
*/
|
|
2464
|
+
async auditMetaItem(request) {
|
|
2465
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
2466
|
+
const limit = Math.min(
|
|
2467
|
+
Math.max(1, request.limit ?? 100),
|
|
2468
|
+
500
|
|
2469
|
+
);
|
|
2470
|
+
try {
|
|
2471
|
+
const where = {
|
|
2472
|
+
type: singular,
|
|
2473
|
+
name: request.name
|
|
2474
|
+
};
|
|
2475
|
+
const rows = await this.engine.find("sys_metadata_audit", {
|
|
2476
|
+
where,
|
|
2477
|
+
orderBy: [{ field: "occurred_at", direction: "desc" }],
|
|
2478
|
+
limit
|
|
2479
|
+
});
|
|
2480
|
+
const events = (Array.isArray(rows) ? rows : []).map((r) => ({
|
|
2481
|
+
id: r.id,
|
|
2482
|
+
occurredAt: typeof r.occurred_at === "string" ? r.occurred_at : r.occurred_at instanceof Date ? r.occurred_at.toISOString() : String(r.occurred_at ?? ""),
|
|
2483
|
+
actor: String(r.actor ?? "system"),
|
|
2484
|
+
source: r.source ?? null,
|
|
2485
|
+
operation: r.operation,
|
|
2486
|
+
outcome: r.outcome,
|
|
2487
|
+
code: String(r.code ?? ""),
|
|
2488
|
+
lockState: r.lock_state ?? null,
|
|
2489
|
+
lockOverridden: Boolean(r.lock_overridden),
|
|
2490
|
+
requestId: r.request_id ?? null,
|
|
2491
|
+
note: r.note ?? null
|
|
2492
|
+
}));
|
|
2493
|
+
return { events };
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
console.warn(
|
|
2496
|
+
`[Protocol] auditMetaItem read failed for ${request.type}/${request.name}: ${err?.message ?? err}`
|
|
2497
|
+
);
|
|
2498
|
+
return { events: [] };
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2110
2501
|
async getUiView(request) {
|
|
2111
2502
|
const schema = this.engine.registry.getObject(request.object);
|
|
2112
2503
|
if (!schema) throw new Error(`Object ${request.object} not found`);
|
|
@@ -2985,9 +3376,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2985
3376
|
for (const tok of raw.split(",")) {
|
|
2986
3377
|
const t = tok.trim();
|
|
2987
3378
|
if (!t) continue;
|
|
2988
|
-
const singular =
|
|
3379
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[t] ?? t;
|
|
2989
3380
|
set.add(singular);
|
|
2990
|
-
const plural =
|
|
3381
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[singular];
|
|
2991
3382
|
if (plural) set.add(plural);
|
|
2992
3383
|
}
|
|
2993
3384
|
this._envWritableTypes = set;
|
|
@@ -2999,13 +3390,201 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2999
3390
|
}
|
|
3000
3391
|
/** Normalize plural→singular before consulting the allow-list. */
|
|
3001
3392
|
static isOverlayAllowed(type) {
|
|
3002
|
-
const singular =
|
|
3393
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3003
3394
|
if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
|
|
3004
3395
|
return true;
|
|
3005
3396
|
}
|
|
3006
3397
|
const env = this.envWritableTypes();
|
|
3007
3398
|
return env.has(singular) || env.has(type);
|
|
3008
3399
|
}
|
|
3400
|
+
/** Does this type permit creating brand-new (artifact-free) items? */
|
|
3401
|
+
static isRuntimeCreateAllowed(type) {
|
|
3402
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3403
|
+
if (this.RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || this.RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
3404
|
+
return true;
|
|
3405
|
+
}
|
|
3406
|
+
if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) {
|
|
3407
|
+
return true;
|
|
3408
|
+
}
|
|
3409
|
+
return false;
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Does an artifact (npm-package-loaded) item exist at `(type, name)`?
|
|
3413
|
+
*
|
|
3414
|
+
* The schema registry's `_packageId` tag is set only when
|
|
3415
|
+
* `registerItem(..., packageId)` is called with a truthy packageId
|
|
3416
|
+
* — and only artifact loaders do that. DB-rehydrated items
|
|
3417
|
+
* (sys_metadata rows registered back into the registry by
|
|
3418
|
+
* `getMetaItems` / `loadMetaFromDb`) call `registerItem` without a
|
|
3419
|
+
* packageId, so they carry no `_packageId` and are correctly
|
|
3420
|
+
* excluded here.
|
|
3421
|
+
*
|
|
3422
|
+
* Used by the two-tier authorization model to distinguish
|
|
3423
|
+
* "overlaying a packaged item" (requires `allowOrgOverride`) from
|
|
3424
|
+
* "authoring a DB-only item" (requires only `allowRuntimeCreate`).
|
|
3425
|
+
*/
|
|
3426
|
+
isArtifactBacked(type, name) {
|
|
3427
|
+
const registry = this.engine?.registry;
|
|
3428
|
+
if (!registry || typeof registry.getItem !== "function") {
|
|
3429
|
+
return false;
|
|
3430
|
+
}
|
|
3431
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3432
|
+
const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3433
|
+
if (!item || !item._packageId) return false;
|
|
3434
|
+
return item._packageId !== "sys_metadata";
|
|
3435
|
+
}
|
|
3436
|
+
// ───────────────────────────────────────────────────────────────────
|
|
3437
|
+
// ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
|
|
3438
|
+
// ───────────────────────────────────────────────────────────────────
|
|
3439
|
+
/**
|
|
3440
|
+
* Look up an item from the artifact registry across both the requested
|
|
3441
|
+
* type and its singular/plural twin. Returns `undefined` when the
|
|
3442
|
+
* registry is unavailable or the item is not artifact-backed.
|
|
3443
|
+
*/
|
|
3444
|
+
lookupArtifactItem(type, name) {
|
|
3445
|
+
const registry = this.engine?.registry;
|
|
3446
|
+
if (!registry || typeof registry.getItem !== "function") return void 0;
|
|
3447
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3448
|
+
return registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* Resolve the effective `_lock` for an item by consulting the
|
|
3452
|
+
* artifact registry first, then the persisted overlay row. Artifact
|
|
3453
|
+
* always wins — by design, an overlay cannot loosen a packaged
|
|
3454
|
+
* lock (ADR-0010 §3.3).
|
|
3455
|
+
*
|
|
3456
|
+
* Returns `'none'` when nothing is locked, which is the common
|
|
3457
|
+
* case. Safe to call when `environmentId` is undefined (control-
|
|
3458
|
+
* plane bootstrap) — the lock check is only meaningful in tenant
|
|
3459
|
+
* scope and the caller is expected to also gate on `environmentId`.
|
|
3460
|
+
*/
|
|
3461
|
+
async getEffectiveLock(type, name, organizationId) {
|
|
3462
|
+
const registry = this.engine?.registry;
|
|
3463
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3464
|
+
let artifactItem;
|
|
3465
|
+
if (registry && typeof registry.getItem === "function") {
|
|
3466
|
+
artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3467
|
+
}
|
|
3468
|
+
if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
|
|
3469
|
+
const p = (0, import_kernel5.extractProtection)(artifactItem);
|
|
3470
|
+
if (p.lock !== "none") {
|
|
3471
|
+
return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
try {
|
|
3475
|
+
const where = {
|
|
3476
|
+
type,
|
|
3477
|
+
name,
|
|
3478
|
+
state: "active",
|
|
3479
|
+
organization_id: organizationId ?? null
|
|
3480
|
+
};
|
|
3481
|
+
const row = await this.engine.findOne("sys_metadata", { where });
|
|
3482
|
+
if (row) {
|
|
3483
|
+
const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
3484
|
+
const p = (0, import_kernel5.extractProtection)(body);
|
|
3485
|
+
if (p.lock !== "none") {
|
|
3486
|
+
return { lock: p.lock, lockReason: p.lockReason, lockSource: "overlay" };
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
} catch {
|
|
3490
|
+
}
|
|
3491
|
+
return { lock: "none", lockReason: void 0, lockSource: void 0 };
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Best-effort audit-row writer (ADR-0010 §3.6). Failures here are
|
|
3495
|
+
* logged but never block the underlying decision: an environment
|
|
3496
|
+
* without the audit table provisioned (legacy installs before this
|
|
3497
|
+
* ADR landed) still answers normal API calls, just without the
|
|
3498
|
+
* compliance trail. Phase 2 will make the audit table a hard
|
|
3499
|
+
* dependency.
|
|
3500
|
+
*/
|
|
3501
|
+
async recordMetadataAudit(entry) {
|
|
3502
|
+
try {
|
|
3503
|
+
await this.engine.insert("sys_metadata_audit", {
|
|
3504
|
+
occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3505
|
+
actor: entry.actor ?? "system",
|
|
3506
|
+
source: entry.source ?? "protocol",
|
|
3507
|
+
type: import_shared3.PLURAL_TO_SINGULAR[entry.type] ?? entry.type,
|
|
3508
|
+
name: entry.name,
|
|
3509
|
+
organization_id: entry.organizationId ?? null,
|
|
3510
|
+
operation: entry.operation,
|
|
3511
|
+
outcome: entry.outcome,
|
|
3512
|
+
code: entry.code,
|
|
3513
|
+
lock_state: entry.lockState ?? "none",
|
|
3514
|
+
lock_overridden: entry.lockOverridden ?? false,
|
|
3515
|
+
request_id: entry.requestId ?? null,
|
|
3516
|
+
note: entry.note ?? null
|
|
3517
|
+
});
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
console.warn(
|
|
3520
|
+
`[Protocol] sys_metadata_audit write failed for ${entry.type}/${entry.name}: ${err?.message ?? err}`
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Phase 1 L3 enforcement for write operations (save / publish /
|
|
3526
|
+
* rollback). Returns null on allow. Returns the structured `Error`
|
|
3527
|
+
* the caller should `throw` on deny — also records the denial in
|
|
3528
|
+
* the audit log so refused attempts are visible in compliance
|
|
3529
|
+
* reports (refused writes never reach sys_metadata_history).
|
|
3530
|
+
*/
|
|
3531
|
+
async assertLockAllowsWrite(args) {
|
|
3532
|
+
if (this.environmentId === void 0) return null;
|
|
3533
|
+
const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
|
|
3534
|
+
const refusal = (0, import_kernel5.evaluateLockForWrite)(state.lock);
|
|
3535
|
+
if (!refusal) return null;
|
|
3536
|
+
const reason = state.lockReason ?? refusal.reason;
|
|
3537
|
+
const err = new Error(
|
|
3538
|
+
`[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
|
|
3539
|
+
);
|
|
3540
|
+
err.code = "item_locked";
|
|
3541
|
+
err.status = 403;
|
|
3542
|
+
err.lock = state.lock;
|
|
3543
|
+
err.lockReason = reason;
|
|
3544
|
+
await this.recordMetadataAudit({
|
|
3545
|
+
type: args.type,
|
|
3546
|
+
name: args.name,
|
|
3547
|
+
organizationId: args.organizationId ?? null,
|
|
3548
|
+
operation: args.operation,
|
|
3549
|
+
outcome: "denied",
|
|
3550
|
+
code: "item_locked",
|
|
3551
|
+
lockState: state.lock,
|
|
3552
|
+
actor: args.actor,
|
|
3553
|
+
source: args.source ?? `protocol.${args.operation}MetaItem`,
|
|
3554
|
+
requestId: args.requestId,
|
|
3555
|
+
note: reason
|
|
3556
|
+
});
|
|
3557
|
+
return err;
|
|
3558
|
+
}
|
|
3559
|
+
/** Counterpart of {@link assertLockAllowsWrite} for delete. */
|
|
3560
|
+
async assertLockAllowsDelete(args) {
|
|
3561
|
+
if (this.environmentId === void 0) return null;
|
|
3562
|
+
const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
|
|
3563
|
+
const refusal = (0, import_kernel5.evaluateLockForDelete)(state.lock);
|
|
3564
|
+
if (!refusal) return null;
|
|
3565
|
+
const reason = state.lockReason ?? refusal.reason;
|
|
3566
|
+
const err = new Error(
|
|
3567
|
+
`[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
|
|
3568
|
+
);
|
|
3569
|
+
err.code = "item_locked";
|
|
3570
|
+
err.status = 403;
|
|
3571
|
+
err.lock = state.lock;
|
|
3572
|
+
err.lockReason = reason;
|
|
3573
|
+
await this.recordMetadataAudit({
|
|
3574
|
+
type: args.type,
|
|
3575
|
+
name: args.name,
|
|
3576
|
+
organizationId: args.organizationId ?? null,
|
|
3577
|
+
operation: "delete",
|
|
3578
|
+
outcome: "denied",
|
|
3579
|
+
code: "item_locked",
|
|
3580
|
+
lockState: state.lock,
|
|
3581
|
+
actor: args.actor,
|
|
3582
|
+
source: args.source ?? "protocol.deleteMetaItem",
|
|
3583
|
+
requestId: args.requestId,
|
|
3584
|
+
note: reason
|
|
3585
|
+
});
|
|
3586
|
+
return err;
|
|
3587
|
+
}
|
|
3009
3588
|
/**
|
|
3010
3589
|
* Mirror an object-type overlay write into the in-memory engine
|
|
3011
3590
|
* registry so subsequent CRUD finds the new schema. Idempotent and
|
|
@@ -3031,16 +3610,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3031
3610
|
if (!request.item) {
|
|
3032
3611
|
throw new Error("Item data is required");
|
|
3033
3612
|
}
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
const
|
|
3037
|
-
|
|
3038
|
-
);
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3613
|
+
const mode = request.mode === "draft" ? "draft" : "publish";
|
|
3614
|
+
if (this.environmentId !== void 0) {
|
|
3615
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
3616
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
3617
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
3618
|
+
if (artifactBacked && !overlayAllowed) {
|
|
3619
|
+
const err = new Error(
|
|
3620
|
+
`[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package and the type has not opted into per-org overlay writes (allowOrgOverride=false). Edit the source artifact and redeploy, or set OBJECTSTACK_METADATA_WRITABLE to grant a runtime escape hatch. See docs/adr/0005-metadata-customization-overlay.md.`
|
|
3621
|
+
);
|
|
3622
|
+
err.code = "not_overridable";
|
|
3623
|
+
err.status = 403;
|
|
3624
|
+
throw err;
|
|
3625
|
+
}
|
|
3626
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
3627
|
+
const err = new Error(
|
|
3628
|
+
`[not_creatable] Metadata type '${request.type}' does not allow runtime creation (allowRuntimeCreate=false, allowOrgOverride=false). New items of this type must be defined in source code.`
|
|
3629
|
+
);
|
|
3630
|
+
err.code = "not_creatable";
|
|
3631
|
+
err.status = 403;
|
|
3632
|
+
throw err;
|
|
3633
|
+
}
|
|
3634
|
+
const lockErr = await this.assertLockAllowsWrite({
|
|
3635
|
+
type: request.type,
|
|
3636
|
+
name: request.name,
|
|
3637
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
3638
|
+
operation: "save",
|
|
3639
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3640
|
+
source: "protocol.saveMetaItem"
|
|
3641
|
+
});
|
|
3642
|
+
if (lockErr) throw lockErr;
|
|
3042
3643
|
}
|
|
3043
|
-
const singularType =
|
|
3644
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3044
3645
|
if (!request.force && (singularType === "object" || singularType === "field")) {
|
|
3045
3646
|
try {
|
|
3046
3647
|
const existing = await this.getMetaItem({
|
|
@@ -3088,8 +3689,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3088
3689
|
}
|
|
3089
3690
|
}
|
|
3090
3691
|
await this.ensureOverlayIndex();
|
|
3091
|
-
const singularTypeForRepo =
|
|
3092
|
-
|
|
3692
|
+
const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3693
|
+
const overlayAllowedForRepo = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
3694
|
+
const runtimeCreateAllowedForRepo = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
3695
|
+
const useRepoPath = overlayAllowedForRepo || runtimeCreateAllowedForRepo;
|
|
3696
|
+
if (useRepoPath) {
|
|
3697
|
+
const artifactBacked = this.isArtifactBacked(singularTypeForRepo, request.name);
|
|
3698
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3093
3699
|
const orgId = request.organizationId ?? null;
|
|
3094
3700
|
const repo = this.getOverlayRepo(orgId);
|
|
3095
3701
|
const ref = {
|
|
@@ -3101,21 +3707,37 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3101
3707
|
if (request.parentVersion !== void 0) {
|
|
3102
3708
|
parentVersion = request.parentVersion;
|
|
3103
3709
|
} else {
|
|
3104
|
-
const current = await repo.get(ref);
|
|
3710
|
+
const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
|
|
3105
3711
|
parentVersion = current?.hash ?? null;
|
|
3106
3712
|
}
|
|
3107
3713
|
try {
|
|
3108
3714
|
const result = await repo.put(ref, request.item, {
|
|
3109
3715
|
parentVersion,
|
|
3110
3716
|
actor: request.actor ?? "system",
|
|
3111
|
-
source: "protocol.saveMetaItem"
|
|
3717
|
+
source: "protocol.saveMetaItem",
|
|
3718
|
+
intent,
|
|
3719
|
+
state: mode === "draft" ? "draft" : "active"
|
|
3720
|
+
});
|
|
3721
|
+
if (mode === "publish") {
|
|
3722
|
+
this.applyObjectRegistryMutation(request);
|
|
3723
|
+
}
|
|
3724
|
+
await this.recordMetadataAudit({
|
|
3725
|
+
type: request.type,
|
|
3726
|
+
name: request.name,
|
|
3727
|
+
organizationId: orgId,
|
|
3728
|
+
operation: "save",
|
|
3729
|
+
outcome: "allowed",
|
|
3730
|
+
code: "ok",
|
|
3731
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3732
|
+
source: "protocol.saveMetaItem",
|
|
3733
|
+
note: mode === "draft" ? "draft" : "active"
|
|
3112
3734
|
});
|
|
3113
|
-
this.applyObjectRegistryMutation(request);
|
|
3114
3735
|
return {
|
|
3115
3736
|
success: true,
|
|
3116
3737
|
version: result.version,
|
|
3117
3738
|
seq: result.seq,
|
|
3118
|
-
|
|
3739
|
+
state: mode === "draft" ? "draft" : "active",
|
|
3740
|
+
message: orgId ? `Saved customization overlay (org=${orgId}, state=${mode === "draft" ? "draft" : "active"}) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]` : `Saved customization overlay (env-wide, state=${mode === "draft" ? "draft" : "active"}) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3119
3741
|
};
|
|
3120
3742
|
} catch (err) {
|
|
3121
3743
|
if (err instanceof import_metadata_core2.ConflictError) {
|
|
@@ -3199,8 +3821,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3199
3821
|
* "no history" uniformly.
|
|
3200
3822
|
*/
|
|
3201
3823
|
async historyMetaItem(request) {
|
|
3202
|
-
const singularType =
|
|
3203
|
-
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
|
|
3824
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3825
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3204
3826
|
return { events: [] };
|
|
3205
3827
|
}
|
|
3206
3828
|
const orgId = request.organizationId ?? null;
|
|
@@ -3218,22 +3840,264 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3218
3840
|
return { events };
|
|
3219
3841
|
}
|
|
3220
3842
|
/**
|
|
3221
|
-
*
|
|
3222
|
-
*
|
|
3223
|
-
*
|
|
3224
|
-
* with {@link saveMetaItem}.
|
|
3843
|
+
* Promote the pending draft overlay to the live (`active`) row.
|
|
3844
|
+
* Records a history event with `op='publish'`. 404 (`[no_draft]`)
|
|
3845
|
+
* when there is nothing to publish.
|
|
3225
3846
|
*/
|
|
3226
|
-
async
|
|
3227
|
-
|
|
3847
|
+
async publishMetaItem(request) {
|
|
3848
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3849
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3850
|
+
const err = new Error(
|
|
3851
|
+
`[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
|
|
3852
|
+
);
|
|
3853
|
+
err.code = "not_overridable";
|
|
3854
|
+
err.status = 403;
|
|
3855
|
+
throw err;
|
|
3856
|
+
}
|
|
3857
|
+
const _publishLockErr = await this.assertLockAllowsWrite({
|
|
3858
|
+
type: request.type,
|
|
3859
|
+
name: request.name,
|
|
3860
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
3861
|
+
operation: "publish",
|
|
3862
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3863
|
+
source: "protocol.publishMetaItem"
|
|
3864
|
+
});
|
|
3865
|
+
if (_publishLockErr) throw _publishLockErr;
|
|
3866
|
+
await this.ensureOverlayIndex();
|
|
3867
|
+
const orgId = request.organizationId ?? null;
|
|
3868
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3869
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3870
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3871
|
+
const ref = {
|
|
3872
|
+
type: singularType,
|
|
3873
|
+
name: request.name,
|
|
3874
|
+
org: orgId ?? "env"
|
|
3875
|
+
};
|
|
3876
|
+
try {
|
|
3877
|
+
const result = await repo.promoteDraft(ref, {
|
|
3878
|
+
actor: request.actor ?? "system",
|
|
3879
|
+
source: "protocol.publishMetaItem",
|
|
3880
|
+
...request.message ? { message: request.message } : {},
|
|
3881
|
+
intent
|
|
3882
|
+
});
|
|
3883
|
+
this.applyObjectRegistryMutation({
|
|
3884
|
+
type: request.type,
|
|
3885
|
+
name: request.name,
|
|
3886
|
+
item: result.item.body
|
|
3887
|
+
});
|
|
3888
|
+
return {
|
|
3889
|
+
success: true,
|
|
3890
|
+
version: result.version,
|
|
3891
|
+
seq: result.seq,
|
|
3892
|
+
message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3893
|
+
};
|
|
3894
|
+
} catch (err) {
|
|
3895
|
+
if (err instanceof import_metadata_core2.ConflictError) {
|
|
3896
|
+
const conflict = new Error(
|
|
3897
|
+
`[metadata_conflict] ${request.type}/${request.name} published row advanced while you held the draft. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
|
|
3898
|
+
);
|
|
3899
|
+
conflict.code = "metadata_conflict";
|
|
3900
|
+
conflict.status = 409;
|
|
3901
|
+
conflict.expectedParent = err.expectedParent;
|
|
3902
|
+
conflict.actualHead = err.actualHead;
|
|
3903
|
+
throw conflict;
|
|
3904
|
+
}
|
|
3905
|
+
throw err;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
/**
|
|
3909
|
+
* Restore the body recorded at history `toVersion` as the new
|
|
3910
|
+
* live row. Writes a history event with `op='revert'`. 404
|
|
3911
|
+
* (`[version_not_found]`) when the target version doesn't exist;
|
|
3912
|
+
* 409 (`[version_not_restorable]`) when the target is a delete
|
|
3913
|
+
* tombstone (no body to bring back).
|
|
3914
|
+
*/
|
|
3915
|
+
async rollbackMetaItem(request) {
|
|
3916
|
+
if (!Number.isFinite(request.toVersion) || request.toVersion < 1) {
|
|
3917
|
+
const err = new Error(
|
|
3918
|
+
`[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`
|
|
3919
|
+
);
|
|
3920
|
+
err.code = "invalid_request";
|
|
3921
|
+
err.status = 400;
|
|
3922
|
+
throw err;
|
|
3923
|
+
}
|
|
3924
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3925
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3228
3926
|
const err = new Error(
|
|
3229
|
-
`[not_overridable] Metadata type '${request.type}'
|
|
3927
|
+
`[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
|
|
3230
3928
|
);
|
|
3231
3929
|
err.code = "not_overridable";
|
|
3232
3930
|
err.status = 403;
|
|
3233
3931
|
throw err;
|
|
3234
3932
|
}
|
|
3235
|
-
const
|
|
3236
|
-
|
|
3933
|
+
const _rollbackLockErr = await this.assertLockAllowsWrite({
|
|
3934
|
+
type: request.type,
|
|
3935
|
+
name: request.name,
|
|
3936
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
3937
|
+
operation: "rollback",
|
|
3938
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3939
|
+
source: "protocol.rollbackMetaItem"
|
|
3940
|
+
});
|
|
3941
|
+
if (_rollbackLockErr) throw _rollbackLockErr;
|
|
3942
|
+
await this.ensureOverlayIndex();
|
|
3943
|
+
const orgId = request.organizationId ?? null;
|
|
3944
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3945
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3946
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3947
|
+
const ref = {
|
|
3948
|
+
type: singularType,
|
|
3949
|
+
name: request.name,
|
|
3950
|
+
org: orgId ?? "env"
|
|
3951
|
+
};
|
|
3952
|
+
try {
|
|
3953
|
+
const result = await repo.restoreVersion(ref, request.toVersion, {
|
|
3954
|
+
actor: request.actor ?? "system",
|
|
3955
|
+
source: "protocol.rollbackMetaItem",
|
|
3956
|
+
...request.message ? { message: request.message } : {},
|
|
3957
|
+
intent
|
|
3958
|
+
});
|
|
3959
|
+
this.applyObjectRegistryMutation({
|
|
3960
|
+
type: request.type,
|
|
3961
|
+
name: request.name,
|
|
3962
|
+
item: result.item.body
|
|
3963
|
+
});
|
|
3964
|
+
return {
|
|
3965
|
+
success: true,
|
|
3966
|
+
version: result.version,
|
|
3967
|
+
seq: result.seq,
|
|
3968
|
+
restoredFromVersion: request.toVersion,
|
|
3969
|
+
message: `Reverted to version ${request.toVersion} \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3970
|
+
};
|
|
3971
|
+
} catch (err) {
|
|
3972
|
+
if (err instanceof import_metadata_core2.ConflictError) {
|
|
3973
|
+
const conflict = new Error(
|
|
3974
|
+
`[metadata_conflict] ${request.type}/${request.name} advanced during rollback. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
|
|
3975
|
+
);
|
|
3976
|
+
conflict.code = "metadata_conflict";
|
|
3977
|
+
conflict.status = 409;
|
|
3978
|
+
conflict.expectedParent = err.expectedParent;
|
|
3979
|
+
conflict.actualHead = err.actualHead;
|
|
3980
|
+
throw conflict;
|
|
3981
|
+
}
|
|
3982
|
+
throw err;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Compute a shallow structural diff between two historical
|
|
3987
|
+
* versions of a metadata item. Either side may be omitted: when
|
|
3988
|
+
* `toVersion` is undefined the current active body is used; when
|
|
3989
|
+
* `fromVersion` is undefined the immediately previous history row
|
|
3990
|
+
* is used. Returns `{ added, removed, changed }` keyed by JSON
|
|
3991
|
+
* pointer-style paths for primitive leaves; nested objects/arrays
|
|
3992
|
+
* are reported as a single change record.
|
|
3993
|
+
*/
|
|
3994
|
+
async diffMetaItem(request) {
|
|
3995
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3996
|
+
const orgId = request.organizationId ?? null;
|
|
3997
|
+
const events = (await this.historyMetaItem({
|
|
3998
|
+
type: singularType,
|
|
3999
|
+
name: request.name,
|
|
4000
|
+
...orgId ? { organizationId: orgId } : {}
|
|
4001
|
+
})).events;
|
|
4002
|
+
const versions = events.map((ev) => ev.version).filter((v) => typeof v === "number");
|
|
4003
|
+
const repo = this.getOverlayRepo(orgId);
|
|
4004
|
+
const fullRef = {
|
|
4005
|
+
type: singularType,
|
|
4006
|
+
name: request.name,
|
|
4007
|
+
org: orgId ?? "env"
|
|
4008
|
+
};
|
|
4009
|
+
const histRows = [];
|
|
4010
|
+
try {
|
|
4011
|
+
const engineAny = this.engine;
|
|
4012
|
+
const rows = await engineAny.find("sys_metadata_history", {
|
|
4013
|
+
where: {
|
|
4014
|
+
organization_id: orgId,
|
|
4015
|
+
type: singularType,
|
|
4016
|
+
name: request.name
|
|
4017
|
+
}
|
|
4018
|
+
});
|
|
4019
|
+
rows.sort((a, b) => (a.version ?? 0) - (b.version ?? 0));
|
|
4020
|
+
for (const r of rows) {
|
|
4021
|
+
const body = r.metadata == null ? null : typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
4022
|
+
histRows.push({ version: r.version ?? 0, body });
|
|
4023
|
+
}
|
|
4024
|
+
} catch {
|
|
4025
|
+
}
|
|
4026
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
4027
|
+
for (const r of histRows) byVersion.set(r.version, r.body);
|
|
4028
|
+
let fromBody = null;
|
|
4029
|
+
let toBody = null;
|
|
4030
|
+
let fromVersion = null;
|
|
4031
|
+
let toVersion = null;
|
|
4032
|
+
if (request.toVersion !== void 0) {
|
|
4033
|
+
toVersion = request.toVersion;
|
|
4034
|
+
toBody = byVersion.get(request.toVersion) ?? null;
|
|
4035
|
+
} else {
|
|
4036
|
+
const current = await repo.get(fullRef, { state: "active" });
|
|
4037
|
+
toBody = current ? current.body : null;
|
|
4038
|
+
toVersion = histRows.length ? histRows[histRows.length - 1].version : null;
|
|
4039
|
+
}
|
|
4040
|
+
if (request.fromVersion !== void 0) {
|
|
4041
|
+
fromVersion = request.fromVersion;
|
|
4042
|
+
fromBody = byVersion.get(request.fromVersion) ?? null;
|
|
4043
|
+
} else if (toVersion !== null) {
|
|
4044
|
+
const sorted = histRows.map((r) => r.version).filter((v) => v < toVersion);
|
|
4045
|
+
if (sorted.length) {
|
|
4046
|
+
fromVersion = sorted[sorted.length - 1];
|
|
4047
|
+
fromBody = byVersion.get(fromVersion) ?? null;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
const diff = diffShallow(fromBody ?? {}, toBody ?? {});
|
|
4051
|
+
const _used = versions;
|
|
4052
|
+
void _used;
|
|
4053
|
+
return {
|
|
4054
|
+
type: request.type,
|
|
4055
|
+
name: request.name,
|
|
4056
|
+
fromVersion,
|
|
4057
|
+
toVersion,
|
|
4058
|
+
...diff
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
/**
|
|
4062
|
+
* Remove a customization overlay row for the given metadata item, so the
|
|
4063
|
+
* next read falls through to the artifact-loaded default. Implements the
|
|
4064
|
+
* "Reset to factory default" semantic from ADR-0005. Whitelist is shared
|
|
4065
|
+
* with {@link saveMetaItem}.
|
|
4066
|
+
*/
|
|
4067
|
+
async deleteMetaItem(request) {
|
|
4068
|
+
if (this.environmentId !== void 0) {
|
|
4069
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
4070
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
4071
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
4072
|
+
if (artifactBacked && !overlayAllowed) {
|
|
4073
|
+
const err = new Error(
|
|
4074
|
+
`[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package and the type has not opted into per-org overlay writes. See docs/adr/0005-metadata-customization-overlay.md.`
|
|
4075
|
+
);
|
|
4076
|
+
err.code = "not_overridable";
|
|
4077
|
+
err.status = 403;
|
|
4078
|
+
throw err;
|
|
4079
|
+
}
|
|
4080
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
4081
|
+
const err = new Error(
|
|
4082
|
+
`[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.`
|
|
4083
|
+
);
|
|
4084
|
+
err.code = "not_creatable";
|
|
4085
|
+
err.status = 403;
|
|
4086
|
+
throw err;
|
|
4087
|
+
}
|
|
4088
|
+
const lockErr = await this.assertLockAllowsDelete({
|
|
4089
|
+
type: request.type,
|
|
4090
|
+
name: request.name,
|
|
4091
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
4092
|
+
...request.actor ? { actor: request.actor } : {},
|
|
4093
|
+
source: "protocol.deleteMetaItem"
|
|
4094
|
+
});
|
|
4095
|
+
if (lockErr) throw lockErr;
|
|
4096
|
+
}
|
|
4097
|
+
const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
4098
|
+
const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
4099
|
+
const runtimeCreateAllowedForRepoDel = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
4100
|
+
const useRepoPath = overlayAllowedForRepoDel || runtimeCreateAllowedForRepoDel;
|
|
3237
4101
|
if (useRepoPath) {
|
|
3238
4102
|
const orgId = request.organizationId ?? null;
|
|
3239
4103
|
const repo = this.getOverlayRepo(orgId);
|
|
@@ -3243,19 +4107,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3243
4107
|
org: orgId ?? "env"
|
|
3244
4108
|
};
|
|
3245
4109
|
try {
|
|
3246
|
-
const
|
|
4110
|
+
const targetState = request.state === "draft" ? "draft" : "active";
|
|
4111
|
+
const current = await repo.get(ref, { state: targetState });
|
|
3247
4112
|
if (!current) {
|
|
3248
4113
|
return {
|
|
3249
4114
|
success: true,
|
|
3250
4115
|
reset: false,
|
|
3251
|
-
message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
|
|
4116
|
+
message: targetState === "draft" ? `No pending draft for ${request.type}/${request.name}.` : `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
|
|
3252
4117
|
};
|
|
3253
4118
|
}
|
|
3254
4119
|
const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
|
|
3255
4120
|
const result = await repo.delete(ref, {
|
|
3256
4121
|
parentVersion,
|
|
3257
4122
|
actor: request.actor ?? "system",
|
|
3258
|
-
source: "protocol.deleteMetaItem"
|
|
4123
|
+
source: "protocol.deleteMetaItem",
|
|
4124
|
+
intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
|
|
4125
|
+
state: targetState
|
|
3259
4126
|
});
|
|
3260
4127
|
if (this.environmentId === void 0) {
|
|
3261
4128
|
try {
|
|
@@ -3270,11 +4137,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3270
4137
|
} catch {
|
|
3271
4138
|
}
|
|
3272
4139
|
}
|
|
4140
|
+
await this.recordMetadataAudit({
|
|
4141
|
+
type: request.type,
|
|
4142
|
+
name: request.name,
|
|
4143
|
+
organizationId: orgId,
|
|
4144
|
+
operation: "delete",
|
|
4145
|
+
outcome: "allowed",
|
|
4146
|
+
code: "ok",
|
|
4147
|
+
...request.actor ? { actor: request.actor } : {},
|
|
4148
|
+
source: "protocol.deleteMetaItem",
|
|
4149
|
+
note: targetState
|
|
4150
|
+
});
|
|
3273
4151
|
return {
|
|
3274
4152
|
success: true,
|
|
3275
4153
|
reset: true,
|
|
3276
4154
|
seq: result.seq,
|
|
3277
|
-
message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
|
|
4155
|
+
message: request.state === "draft" ? `Draft discarded \u2014 ${request.type}/${request.name}. [seq=${result.seq}]` : `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
|
|
3278
4156
|
};
|
|
3279
4157
|
} catch (err) {
|
|
3280
4158
|
if (err instanceof import_metadata_core2.ConflictError) {
|
|
@@ -3352,7 +4230,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3352
4230
|
for (const record of records) {
|
|
3353
4231
|
try {
|
|
3354
4232
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
3355
|
-
const normalizedType =
|
|
4233
|
+
const normalizedType = import_shared3.PLURAL_TO_SINGULAR[record.type] ?? record.type;
|
|
3356
4234
|
if (normalizedType === "object") {
|
|
3357
4235
|
this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
|
|
3358
4236
|
} else {
|
|
@@ -3386,7 +4264,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3386
4264
|
* — the engine never throws.
|
|
3387
4265
|
*/
|
|
3388
4266
|
async findReferencesToMeta(request) {
|
|
3389
|
-
const singularTarget =
|
|
4267
|
+
const singularTarget = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3390
4268
|
const targetName = request.name;
|
|
3391
4269
|
const matchers = REFERENCE_PATHS[singularTarget];
|
|
3392
4270
|
if (!matchers || matchers.length === 0) {
|
|
@@ -3577,10 +4455,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3577
4455
|
*/
|
|
3578
4456
|
_ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
3579
4457
|
const out = /* @__PURE__ */ new Set();
|
|
3580
|
-
for (const entry of
|
|
4458
|
+
for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
|
|
3581
4459
|
if (!entry.allowOrgOverride) continue;
|
|
3582
4460
|
out.add(entry.type);
|
|
3583
|
-
const plural =
|
|
4461
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
|
|
3584
4462
|
if (plural) out.add(plural);
|
|
3585
4463
|
}
|
|
3586
4464
|
return out;
|
|
@@ -3597,13 +4475,48 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
|
3597
4475
|
* {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
|
|
3598
4476
|
*/
|
|
3599
4477
|
_ObjectStackProtocolImplementation._envWritableTypes = null;
|
|
4478
|
+
/**
|
|
4479
|
+
* Types that opt into runtime creation of brand-new items (ADR-0005
|
|
4480
|
+
* extension — two-tier model). A type may have
|
|
4481
|
+
* `allowOrgOverride: false` (cannot overlay artifact-shipped items)
|
|
4482
|
+
* yet still set `allowRuntimeCreate: true` (users can author new
|
|
4483
|
+
* items in `sys_metadata`). The two flags are orthogonal; see
|
|
4484
|
+
* {@link isArtifactBacked} for how the protocol decides which gate
|
|
4485
|
+
* applies to a given save/delete.
|
|
4486
|
+
*/
|
|
4487
|
+
/**
|
|
4488
|
+
* Set of type names that have a static entry in
|
|
4489
|
+
* `DEFAULT_METADATA_TYPE_REGISTRY`. Anything outside this set is
|
|
4490
|
+
* runtime-registered (plugin-provided types like `theme`, `api`,
|
|
4491
|
+
* `connector`) — the listing endpoint at `getMetaTypes()` synthesises
|
|
4492
|
+
* those with `allowRuntimeCreate: true`, so this gate must agree.
|
|
4493
|
+
*/
|
|
4494
|
+
_ObjectStackProtocolImplementation.STATIC_REGISTRY_TYPES = (() => {
|
|
4495
|
+
const out = /* @__PURE__ */ new Set();
|
|
4496
|
+
for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
|
|
4497
|
+
out.add(entry.type);
|
|
4498
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
|
|
4499
|
+
if (plural) out.add(plural);
|
|
4500
|
+
}
|
|
4501
|
+
return out;
|
|
4502
|
+
})();
|
|
4503
|
+
_ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
|
|
4504
|
+
const out = /* @__PURE__ */ new Set();
|
|
4505
|
+
for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
|
|
4506
|
+
if (!entry.allowRuntimeCreate) continue;
|
|
4507
|
+
out.add(entry.type);
|
|
4508
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
|
|
4509
|
+
if (plural) out.add(plural);
|
|
4510
|
+
}
|
|
4511
|
+
return out;
|
|
4512
|
+
})();
|
|
3600
4513
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
3601
4514
|
|
|
3602
4515
|
// src/engine.ts
|
|
3603
|
-
var
|
|
4516
|
+
var import_kernel6 = require("@objectstack/spec/kernel");
|
|
3604
4517
|
var import_core = require("@objectstack/core");
|
|
3605
4518
|
var import_system2 = require("@objectstack/spec/system");
|
|
3606
|
-
var
|
|
4519
|
+
var import_shared4 = require("@objectstack/spec/shared");
|
|
3607
4520
|
var import_formula2 = require("@objectstack/formula");
|
|
3608
4521
|
|
|
3609
4522
|
// src/hook-wrappers.ts
|
|
@@ -4874,9 +5787,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
4874
5787
|
const itemName = resolveMetadataItemName(key, item);
|
|
4875
5788
|
if (itemName) {
|
|
4876
5789
|
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
4877
|
-
this._registry.registerItem((0,
|
|
5790
|
+
this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", id);
|
|
4878
5791
|
} else {
|
|
4879
|
-
this.logger.warn(`Skipping ${(0,
|
|
5792
|
+
this.logger.warn(`Skipping ${(0, import_shared4.pluralToSingular)(key)} without a derivable name`, { id });
|
|
4880
5793
|
}
|
|
4881
5794
|
}
|
|
4882
5795
|
}
|
|
@@ -5003,7 +5916,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5003
5916
|
const itemName = resolveMetadataItemName(key, item);
|
|
5004
5917
|
if (itemName) {
|
|
5005
5918
|
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
5006
|
-
this._registry.registerItem((0,
|
|
5919
|
+
this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", ownerId);
|
|
5007
5920
|
}
|
|
5008
5921
|
}
|
|
5009
5922
|
}
|
|
@@ -5889,7 +6802,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5889
6802
|
*/
|
|
5890
6803
|
createContext(ctx) {
|
|
5891
6804
|
return new ScopedContext(
|
|
5892
|
-
|
|
6805
|
+
import_kernel6.ExecutionContextSchema.parse(ctx),
|
|
5893
6806
|
this
|
|
5894
6807
|
);
|
|
5895
6808
|
}
|