@objectstack/objectql 6.9.0 → 7.1.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 +260 -7
- package/dist/index.d.ts +260 -7
- package/dist/index.js +758 -184
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +750 -176
- 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
|
}
|
|
@@ -676,6 +684,12 @@ var import_shared = require("@objectstack/spec/shared");
|
|
|
676
684
|
var OVERLAY_ALLOWED_TYPES = new Set(
|
|
677
685
|
import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
|
|
678
686
|
);
|
|
687
|
+
var STATIC_REGISTRY_TYPES = new Set(
|
|
688
|
+
import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)
|
|
689
|
+
);
|
|
690
|
+
var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
|
|
691
|
+
import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type)
|
|
692
|
+
);
|
|
679
693
|
var _envWritableMetadataTypes = null;
|
|
680
694
|
function envWritableMetadataTypes() {
|
|
681
695
|
if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
|
|
@@ -725,11 +739,16 @@ var SysMetadataRepository = class {
|
|
|
725
739
|
/**
|
|
726
740
|
* Read the current overlay row. Returns null if no row exists —
|
|
727
741
|
* callers (e.g. LayeredRepository) fall through to lower layers.
|
|
742
|
+
*
|
|
743
|
+
* `opts.state` selects which lifecycle row to read: defaults to the
|
|
744
|
+
* live published row (`'active'`). Pass `'draft'` to read the pending
|
|
745
|
+
* unpublished revision (if any).
|
|
728
746
|
*/
|
|
729
|
-
async get(ref) {
|
|
747
|
+
async get(ref, opts) {
|
|
730
748
|
this.assertOpen();
|
|
749
|
+
const state = opts?.state ?? "active";
|
|
731
750
|
const row = await this.engine.findOne("sys_metadata", {
|
|
732
|
-
where: this.whereFor(ref)
|
|
751
|
+
where: this.whereFor(ref, state)
|
|
733
752
|
});
|
|
734
753
|
if (!row) return null;
|
|
735
754
|
return this.rowToItem(ref, row);
|
|
@@ -772,12 +791,13 @@ var SysMetadataRepository = class {
|
|
|
772
791
|
}
|
|
773
792
|
async put(ref, spec, opts) {
|
|
774
793
|
this.assertOpen();
|
|
775
|
-
this.assertAllowed(ref.type);
|
|
794
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
795
|
+
const state = opts.state ?? "active";
|
|
776
796
|
const body = spec ?? {};
|
|
777
797
|
const hash = (0, import_metadata_core.hashSpec)(body);
|
|
778
798
|
const result = await this.withTxn(async (ctx) => {
|
|
779
799
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
780
|
-
where: this.whereFor(ref),
|
|
800
|
+
where: this.whereFor(ref, state),
|
|
781
801
|
context: ctx
|
|
782
802
|
});
|
|
783
803
|
const existingHash = existing?.checksum ?? null;
|
|
@@ -789,7 +809,8 @@ var SysMetadataRepository = class {
|
|
|
789
809
|
return { skipped: true, version: hash, seq: item2.seq, item: item2 };
|
|
790
810
|
}
|
|
791
811
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
792
|
-
const
|
|
812
|
+
const baseOp = existing ? "update" : "create";
|
|
813
|
+
const op = opts.opType ?? baseOp;
|
|
793
814
|
const version = await this.nextItemVersion(ref, ctx);
|
|
794
815
|
const eventSeq = await this.nextEventSeq(ctx);
|
|
795
816
|
const parentRowData = {
|
|
@@ -798,7 +819,7 @@ var SysMetadataRepository = class {
|
|
|
798
819
|
organization_id: this.organizationId,
|
|
799
820
|
metadata: JSON.stringify(body),
|
|
800
821
|
checksum: hash,
|
|
801
|
-
state
|
|
822
|
+
state,
|
|
802
823
|
version,
|
|
803
824
|
updated_at: now
|
|
804
825
|
};
|
|
@@ -864,25 +885,28 @@ var SysMetadataRepository = class {
|
|
|
864
885
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
865
886
|
}
|
|
866
887
|
this.seqCounter = result.seq;
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
888
|
+
if (state === "active") {
|
|
889
|
+
this.broadcast({
|
|
890
|
+
seq: result.seq,
|
|
891
|
+
op: result.op,
|
|
892
|
+
ref: this.fullRef(ref),
|
|
893
|
+
hash: result.version,
|
|
894
|
+
parentHash: result.existingHash,
|
|
895
|
+
actor: result.actor,
|
|
896
|
+
message: result.message,
|
|
897
|
+
ts: result.now,
|
|
898
|
+
source: result.source
|
|
899
|
+
});
|
|
900
|
+
}
|
|
878
901
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
879
902
|
}
|
|
880
903
|
async delete(ref, opts) {
|
|
881
904
|
this.assertOpen();
|
|
882
|
-
this.assertAllowed(ref.type);
|
|
905
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
906
|
+
const state = opts.state ?? "active";
|
|
883
907
|
const result = await this.withTxn(async (ctx) => {
|
|
884
908
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
885
|
-
where: this.whereFor(ref),
|
|
909
|
+
where: this.whereFor(ref, state),
|
|
886
910
|
context: ctx
|
|
887
911
|
});
|
|
888
912
|
if (!existing) {
|
|
@@ -899,32 +923,38 @@ var SysMetadataRepository = class {
|
|
|
899
923
|
);
|
|
900
924
|
}
|
|
901
925
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
902
|
-
|
|
903
|
-
|
|
926
|
+
let version = 0;
|
|
927
|
+
let eventSeq = 0;
|
|
928
|
+
if (state === "active") {
|
|
929
|
+
version = await this.nextItemVersion(ref, ctx);
|
|
930
|
+
eventSeq = await this.nextEventSeq(ctx);
|
|
931
|
+
}
|
|
904
932
|
await this.engine.delete("sys_metadata", {
|
|
905
933
|
where: { id: existingId },
|
|
906
934
|
context: ctx
|
|
907
935
|
});
|
|
908
|
-
|
|
909
|
-
this.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
936
|
+
if (state === "active") {
|
|
937
|
+
await this.engine.insert(
|
|
938
|
+
this.historyTable,
|
|
939
|
+
{
|
|
940
|
+
id: this.uuid(),
|
|
941
|
+
event_seq: eventSeq,
|
|
942
|
+
type: ref.type,
|
|
943
|
+
name: ref.name,
|
|
944
|
+
version,
|
|
945
|
+
operation_type: "delete",
|
|
946
|
+
metadata: null,
|
|
947
|
+
checksum: null,
|
|
948
|
+
previous_checksum: existingHash,
|
|
949
|
+
change_note: opts.message,
|
|
950
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
951
|
+
organization_id: this.organizationId,
|
|
952
|
+
recorded_by: opts.actor,
|
|
953
|
+
recorded_at: now
|
|
954
|
+
},
|
|
955
|
+
{ context: ctx }
|
|
956
|
+
);
|
|
957
|
+
}
|
|
928
958
|
return {
|
|
929
959
|
eventSeq,
|
|
930
960
|
existingHash,
|
|
@@ -934,20 +964,117 @@ var SysMetadataRepository = class {
|
|
|
934
964
|
actor: opts.actor
|
|
935
965
|
};
|
|
936
966
|
});
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
967
|
+
if (state === "active") {
|
|
968
|
+
this.seqCounter = result.eventSeq;
|
|
969
|
+
this.broadcast({
|
|
970
|
+
seq: result.eventSeq,
|
|
971
|
+
op: "delete",
|
|
972
|
+
ref: this.fullRef(ref),
|
|
973
|
+
hash: null,
|
|
974
|
+
parentHash: result.existingHash,
|
|
975
|
+
actor: result.actor,
|
|
976
|
+
message: result.message,
|
|
977
|
+
ts: result.now,
|
|
978
|
+
source: result.source
|
|
979
|
+
});
|
|
980
|
+
}
|
|
949
981
|
return { seq: result.eventSeq };
|
|
950
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Promote the pending draft row for `ref` into the live (`active`)
|
|
985
|
+
* overlay. Atomic: reads the draft inside the same transaction, runs
|
|
986
|
+
* the canonical `put` to upsert the active row (which appends a
|
|
987
|
+
* history event with `operation_type='publish'`), then deletes the
|
|
988
|
+
* draft row.
|
|
989
|
+
*
|
|
990
|
+
* Errors if no draft exists (callers should 404). The active row's
|
|
991
|
+
* `parentVersion` is computed from the current active hash so this
|
|
992
|
+
* also surfaces optimistic-lock conflicts when something else has
|
|
993
|
+
* published in between (e.g. another admin reverted to an older
|
|
994
|
+
* version since the draft was authored).
|
|
995
|
+
*/
|
|
996
|
+
async promoteDraft(ref, opts) {
|
|
997
|
+
this.assertOpen();
|
|
998
|
+
const draft = await this.get(ref, { state: "draft" });
|
|
999
|
+
if (!draft) {
|
|
1000
|
+
const err = new Error(
|
|
1001
|
+
`[no_draft] No pending draft exists for ${ref.type}/${ref.name} \u2014 nothing to publish.`
|
|
1002
|
+
);
|
|
1003
|
+
err.code = "no_draft";
|
|
1004
|
+
err.status = 404;
|
|
1005
|
+
throw err;
|
|
1006
|
+
}
|
|
1007
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
1008
|
+
const result = await this.put(ref, draft.body, {
|
|
1009
|
+
parentVersion: currentActive?.hash ?? null,
|
|
1010
|
+
actor: opts.actor,
|
|
1011
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
1012
|
+
message: opts.message ?? `publish draft (hash ${draft.hash})`,
|
|
1013
|
+
intent: opts.intent ?? "override-artifact",
|
|
1014
|
+
state: "active",
|
|
1015
|
+
opType: "publish"
|
|
1016
|
+
});
|
|
1017
|
+
try {
|
|
1018
|
+
await this.delete(ref, {
|
|
1019
|
+
parentVersion: draft.hash,
|
|
1020
|
+
actor: opts.actor,
|
|
1021
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
1022
|
+
intent: opts.intent ?? "override-artifact",
|
|
1023
|
+
state: "draft"
|
|
1024
|
+
});
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
return result;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Restore the body recorded in history at `targetVersion` (per-org
|
|
1031
|
+
* lineage counter) as the new active row. Writes a history event
|
|
1032
|
+
* with `operation_type='revert'` so the audit trail captures the
|
|
1033
|
+
* intent. Does NOT touch any draft row.
|
|
1034
|
+
*
|
|
1035
|
+
* Throws `[version_not_found]` (404) if the target version row is
|
|
1036
|
+
* missing or is a delete tombstone (no body to restore).
|
|
1037
|
+
*/
|
|
1038
|
+
async restoreVersion(ref, targetVersion, opts) {
|
|
1039
|
+
this.assertOpen();
|
|
1040
|
+
const full = this.fullRef(ref);
|
|
1041
|
+
const row = await this.engine.findOne(this.historyTable, {
|
|
1042
|
+
where: {
|
|
1043
|
+
organization_id: this.organizationId,
|
|
1044
|
+
type: full.type,
|
|
1045
|
+
name: full.name,
|
|
1046
|
+
version: targetVersion
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
if (!row) {
|
|
1050
|
+
const err = new Error(
|
|
1051
|
+
`[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`
|
|
1052
|
+
);
|
|
1053
|
+
err.code = "version_not_found";
|
|
1054
|
+
err.status = 404;
|
|
1055
|
+
throw err;
|
|
1056
|
+
}
|
|
1057
|
+
const raw = row.metadata;
|
|
1058
|
+
if (raw === null || raw === void 0) {
|
|
1059
|
+
const err = new Error(
|
|
1060
|
+
`[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone \u2014 nothing to restore.`
|
|
1061
|
+
);
|
|
1062
|
+
err.code = "version_not_restorable";
|
|
1063
|
+
err.status = 409;
|
|
1064
|
+
throw err;
|
|
1065
|
+
}
|
|
1066
|
+
const body = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1067
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
1068
|
+
return this.put(ref, body, {
|
|
1069
|
+
parentVersion: currentActive?.hash ?? null,
|
|
1070
|
+
actor: opts.actor,
|
|
1071
|
+
source: opts.source ?? "sys-metadata-repo.revert",
|
|
1072
|
+
message: opts.message ?? `revert to version ${targetVersion}`,
|
|
1073
|
+
intent: opts.intent ?? "override-artifact",
|
|
1074
|
+
state: "active",
|
|
1075
|
+
opType: "revert"
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
951
1078
|
async *list(filter) {
|
|
952
1079
|
this.assertOpen();
|
|
953
1080
|
const where = {
|
|
@@ -1002,6 +1129,7 @@ var SysMetadataRepository = class {
|
|
|
1002
1129
|
ref: full,
|
|
1003
1130
|
hash: row.checksum ?? null,
|
|
1004
1131
|
parentHash: row.previous_checksum ?? null,
|
|
1132
|
+
version: typeof row.version === "number" ? row.version : void 0,
|
|
1005
1133
|
actor: row.recorded_by ?? "unknown",
|
|
1006
1134
|
message: row.change_note ?? void 0,
|
|
1007
1135
|
ts: row.recorded_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
@@ -1083,29 +1211,52 @@ var SysMetadataRepository = class {
|
|
|
1083
1211
|
assertOpen() {
|
|
1084
1212
|
if (this.closed) throw new Error("SysMetadataRepository is closed");
|
|
1085
1213
|
}
|
|
1086
|
-
|
|
1214
|
+
/**
|
|
1215
|
+
* Defense-in-depth authorization gate.
|
|
1216
|
+
*
|
|
1217
|
+
* `intent` defaults to `'override-artifact'` (the historical strict
|
|
1218
|
+
* behavior). The protocol layer passes `'runtime-only'` after it has
|
|
1219
|
+
* verified — via the schema registry — that no artifact item exists
|
|
1220
|
+
* at `(type, name)`. In that case we accept types with
|
|
1221
|
+
* `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
|
|
1222
|
+
*
|
|
1223
|
+
* The env-var escape hatch (`OBJECTSTACK_METADATA_WRITABLE`) still
|
|
1224
|
+
* applies to BOTH intents, so operators can opt into artifact
|
|
1225
|
+
* overrides at runtime for emergency fixes.
|
|
1226
|
+
*/
|
|
1227
|
+
assertAllowed(type, intent = "override-artifact") {
|
|
1087
1228
|
const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
|
|
1088
1229
|
const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
|
|
1089
1230
|
if (allowedByRegistry) return;
|
|
1231
|
+
if (intent === "runtime-only") {
|
|
1232
|
+
if (RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (!STATIC_REGISTRY_TYPES.has(singular) && !STATIC_REGISTRY_TYPES.has(type)) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1090
1239
|
const env = envWritableMetadataTypes();
|
|
1091
1240
|
if (env.has(singular) || env.has(type)) return;
|
|
1092
1241
|
const allowed = [
|
|
1093
1242
|
...OVERLAY_ALLOWED_TYPES,
|
|
1094
1243
|
...envWritableMetadataTypes()
|
|
1095
1244
|
];
|
|
1245
|
+
const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
|
|
1246
|
+
const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
|
|
1096
1247
|
const err = new Error(
|
|
1097
|
-
`[
|
|
1248
|
+
`[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
|
|
1098
1249
|
);
|
|
1099
|
-
err.code =
|
|
1250
|
+
err.code = code;
|
|
1100
1251
|
err.status = 403;
|
|
1101
1252
|
throw err;
|
|
1102
1253
|
}
|
|
1103
|
-
whereFor(ref) {
|
|
1254
|
+
whereFor(ref, state = "active") {
|
|
1104
1255
|
return {
|
|
1105
1256
|
type: ref.type,
|
|
1106
1257
|
name: ref.name,
|
|
1107
1258
|
organization_id: this.organizationId,
|
|
1108
|
-
state
|
|
1259
|
+
state
|
|
1109
1260
|
};
|
|
1110
1261
|
}
|
|
1111
1262
|
fullRef(ref) {
|
|
@@ -1202,57 +1353,58 @@ var SysMetadataRepository = class {
|
|
|
1202
1353
|
// src/protocol.ts
|
|
1203
1354
|
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
1204
1355
|
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");
|
|
1356
|
+
var import_shared3 = require("@objectstack/spec/shared");
|
|
1209
1357
|
var import_system = require("@objectstack/spec/system");
|
|
1210
|
-
var
|
|
1211
|
-
var import_automation = require("@objectstack/spec/automation");
|
|
1212
|
-
var import_kernel3 = require("@objectstack/spec/kernel");
|
|
1358
|
+
var import_kernel4 = require("@objectstack/spec/kernel");
|
|
1213
1359
|
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
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1360
|
+
|
|
1361
|
+
// src/metadata-diagnostics.ts
|
|
1362
|
+
var import_kernel3 = require("@objectstack/spec/kernel");
|
|
1363
|
+
var import_shared2 = require("@objectstack/spec/shared");
|
|
1364
|
+
function computeMetadataDiagnostics(type, item) {
|
|
1365
|
+
const singular = import_shared2.PLURAL_TO_SINGULAR[type] ?? type;
|
|
1366
|
+
const schema = (0, import_kernel3.getMetadataTypeSchema)(singular);
|
|
1367
|
+
if (!schema) return void 0;
|
|
1368
|
+
if (item === null || item === void 0 || typeof item !== "object") {
|
|
1369
|
+
return {
|
|
1370
|
+
valid: false,
|
|
1371
|
+
errors: [{
|
|
1372
|
+
path: "",
|
|
1373
|
+
message: "Metadata document must be a non-null object",
|
|
1374
|
+
code: "invalid_type"
|
|
1375
|
+
}]
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
const candidate = "_diagnostics" in item ? stripDiagnostics(item) : item;
|
|
1379
|
+
const parsed = schema.safeParse(candidate);
|
|
1380
|
+
if (parsed.success) {
|
|
1381
|
+
return { valid: true };
|
|
1382
|
+
}
|
|
1383
|
+
const errors = parsed.error.issues.map((issue) => ({
|
|
1384
|
+
path: issue.path.map(String).join("."),
|
|
1385
|
+
message: issue.message,
|
|
1386
|
+
code: issue.code
|
|
1387
|
+
}));
|
|
1388
|
+
return { valid: false, errors };
|
|
1389
|
+
}
|
|
1390
|
+
function stripDiagnostics(item) {
|
|
1391
|
+
const { _diagnostics: _drop, ...rest } = item;
|
|
1392
|
+
void _drop;
|
|
1393
|
+
return rest;
|
|
1394
|
+
}
|
|
1395
|
+
function decorateMetadataItem(type, item) {
|
|
1396
|
+
if (!item || typeof item !== "object") return item;
|
|
1397
|
+
const diagnostics = computeMetadataDiagnostics(type, item);
|
|
1398
|
+
if (!diagnostics) return item;
|
|
1399
|
+
return { ...item, _diagnostics: diagnostics };
|
|
1400
|
+
}
|
|
1401
|
+
function decorateMetadataItems(type, items) {
|
|
1402
|
+
if (!Array.isArray(items)) return items;
|
|
1403
|
+
return items.map((item) => decorateMetadataItem(type, item));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/protocol.ts
|
|
1407
|
+
var TYPE_TO_FORM = import_system.METADATA_FORM_REGISTRY;
|
|
1256
1408
|
var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
1257
1409
|
function toJsonSchemaSafe(schema) {
|
|
1258
1410
|
const cached = _jsonSchemaCache.get(schema);
|
|
@@ -1281,9 +1433,17 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1281
1433
|
abstract: { type: "boolean", default: false },
|
|
1282
1434
|
datasource: { type: "string" },
|
|
1283
1435
|
fields: {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1436
|
+
// Canonical Object.fields is a name-keyed map
|
|
1437
|
+
// (Record<string, FieldDefinition>) — insertion order is
|
|
1438
|
+
// display order. The SchemaForm engine recognises
|
|
1439
|
+
// `additionalProperties` as a Record and dispatches to
|
|
1440
|
+
// the `record` form-field renderer (ADR-0007). The form
|
|
1441
|
+
// layout in `object.form.ts` declares `type: 'record'`
|
|
1442
|
+
// so the inner `additionalProperties` schema is used to
|
|
1443
|
+
// shape each value.
|
|
1444
|
+
type: "object",
|
|
1445
|
+
default: {},
|
|
1446
|
+
additionalProperties: {
|
|
1287
1447
|
type: "object",
|
|
1288
1448
|
properties: {
|
|
1289
1449
|
name: { type: "string" },
|
|
@@ -1294,7 +1454,7 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1294
1454
|
defaultValue: {},
|
|
1295
1455
|
description: { type: "string" }
|
|
1296
1456
|
},
|
|
1297
|
-
required: ["
|
|
1457
|
+
required: ["type"]
|
|
1298
1458
|
}
|
|
1299
1459
|
},
|
|
1300
1460
|
capabilities: { type: "object", additionalProperties: true }
|
|
@@ -1439,19 +1599,9 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1439
1599
|
additionalProperties: true
|
|
1440
1600
|
}
|
|
1441
1601
|
};
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
switch (singular) {
|
|
1446
|
-
case "view": {
|
|
1447
|
-
const t = item && typeof item === "object" && "type" in item ? String(item.type) : void 0;
|
|
1448
|
-
return t && FORM_VIEW_TYPES.has(t) ? import_ui2.FormViewSchema : import_ui2.ListViewSchema;
|
|
1449
|
-
}
|
|
1450
|
-
case "dashboard":
|
|
1451
|
-
return import_ui2.DashboardSchema;
|
|
1452
|
-
default:
|
|
1453
|
-
return null;
|
|
1454
|
-
}
|
|
1602
|
+
function resolveOverlaySchema(type, _item) {
|
|
1603
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
1604
|
+
return (0, import_kernel4.getMetadataTypeSchema)(singular) ?? null;
|
|
1455
1605
|
}
|
|
1456
1606
|
function simpleHash(str) {
|
|
1457
1607
|
let hash = 0;
|
|
@@ -1584,6 +1734,32 @@ function extractPathValues(item, path) {
|
|
|
1584
1734
|
}
|
|
1585
1735
|
return out;
|
|
1586
1736
|
}
|
|
1737
|
+
function diffShallow(from, to) {
|
|
1738
|
+
const added = [];
|
|
1739
|
+
const removed = [];
|
|
1740
|
+
const changed = [];
|
|
1741
|
+
const fromKeys = new Set(Object.keys(from ?? {}));
|
|
1742
|
+
const toKeys = new Set(Object.keys(to ?? {}));
|
|
1743
|
+
for (const k of toKeys) {
|
|
1744
|
+
if (!fromKeys.has(k)) {
|
|
1745
|
+
added.push({ path: k, value: to[k] });
|
|
1746
|
+
} else {
|
|
1747
|
+
const a = from[k];
|
|
1748
|
+
const b = to[k];
|
|
1749
|
+
const aStr = JSON.stringify(a);
|
|
1750
|
+
const bStr = JSON.stringify(b);
|
|
1751
|
+
if (aStr !== bStr) {
|
|
1752
|
+
changed.push({ path: k, from: a, to: b });
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
for (const k of fromKeys) {
|
|
1757
|
+
if (!toKeys.has(k)) {
|
|
1758
|
+
removed.push({ path: k, value: from[k] });
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return { added, removed, changed };
|
|
1762
|
+
}
|
|
1587
1763
|
function detectDestructiveObjectChanges(prev, next) {
|
|
1588
1764
|
if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
|
|
1589
1765
|
const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
|
|
@@ -1715,6 +1891,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1715
1891
|
}
|
|
1716
1892
|
}
|
|
1717
1893
|
}
|
|
1894
|
+
const draftPartialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id) WHERE state = 'draft'";
|
|
1895
|
+
try {
|
|
1896
|
+
await exec(draftPartialSql);
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1899
|
+
if (/partial|where clause|syntax/i.test(msg)) {
|
|
1900
|
+
try {
|
|
1901
|
+
await exec(
|
|
1902
|
+
"CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id)"
|
|
1903
|
+
);
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1718
1908
|
} catch {
|
|
1719
1909
|
}
|
|
1720
1910
|
}
|
|
@@ -1835,11 +2025,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1835
2025
|
const allTypes = Array.from(/* @__PURE__ */ new Set([...schemaTypes, ...runtimeTypes]));
|
|
1836
2026
|
const writableOverrides = _ObjectStackProtocolImplementation.envWritableTypes();
|
|
1837
2027
|
const registryByType = new Map(
|
|
1838
|
-
|
|
2028
|
+
import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])
|
|
1839
2029
|
);
|
|
1840
2030
|
const entries = allTypes.map((type) => {
|
|
1841
|
-
const singular =
|
|
1842
|
-
const zodSchema =
|
|
2031
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
2032
|
+
const zodSchema = (0, import_kernel4.getMetadataTypeSchema)(singular);
|
|
1843
2033
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
1844
2034
|
const form = TYPE_TO_FORM[singular];
|
|
1845
2035
|
const base = registryByType.get(singular);
|
|
@@ -1880,19 +2070,76 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1880
2070
|
});
|
|
1881
2071
|
return { types: allTypes, entries };
|
|
1882
2072
|
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Sweep all (or filtered) metadata types and report entries that
|
|
2075
|
+
* fail spec validation. Powers the Studio governance view
|
|
2076
|
+
* (`GET /api/v1/meta/diagnostics`) and `os doctor`-style CLI
|
|
2077
|
+
* checks.
|
|
2078
|
+
*
|
|
2079
|
+
* `severity` defaults to `'error'` — only entries with at least
|
|
2080
|
+
* one Zod error issue are returned. `'warning'` includes
|
|
2081
|
+
* everything we surface (warnings are reserved for a future lint
|
|
2082
|
+
* layer on top of spec validation).
|
|
2083
|
+
*
|
|
2084
|
+
* `type` may be either a singular (`'view'`) or plural (`'views'`)
|
|
2085
|
+
* identifier; the underlying `getMetaItems` already normalises.
|
|
2086
|
+
*
|
|
2087
|
+
* Implementation note: leverages the `_diagnostics` already
|
|
2088
|
+
* decorated onto items by `getMetaItems()` to avoid running
|
|
2089
|
+
* `safeParse()` twice. For types whose schema is unregistered we
|
|
2090
|
+
* skip silently (they cannot be validated and should not appear
|
|
2091
|
+
* as "valid" either — they are simply opaque to this report).
|
|
2092
|
+
*/
|
|
2093
|
+
async getMetaDiagnostics(request = {}) {
|
|
2094
|
+
const includeWarnings = request.severity === "warning";
|
|
2095
|
+
const targetTypes = request.type ? [request.type] : import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => (0, import_kernel4.getMetadataTypeSchema)(e.type)).map((e) => e.type);
|
|
2096
|
+
const entries = [];
|
|
2097
|
+
let scannedItems = 0;
|
|
2098
|
+
for (const t of targetTypes) {
|
|
2099
|
+
let listed;
|
|
2100
|
+
try {
|
|
2101
|
+
listed = await this.getMetaItems({
|
|
2102
|
+
type: t,
|
|
2103
|
+
organizationId: request.organizationId,
|
|
2104
|
+
packageId: request.packageId
|
|
2105
|
+
});
|
|
2106
|
+
} catch {
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
|
|
2110
|
+
for (const item of items) {
|
|
2111
|
+
scannedItems += 1;
|
|
2112
|
+
const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
|
|
2113
|
+
if (!diag) continue;
|
|
2114
|
+
if (diag.valid && !includeWarnings) continue;
|
|
2115
|
+
if (diag.valid && includeWarnings && !diag.warnings?.length) continue;
|
|
2116
|
+
entries.push({
|
|
2117
|
+
type: t,
|
|
2118
|
+
name: typeof item?.name === "string" ? item.name : "<unknown>",
|
|
2119
|
+
diagnostics: diag
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return {
|
|
2124
|
+
entries,
|
|
2125
|
+
total: entries.length,
|
|
2126
|
+
scannedTypes: targetTypes.length,
|
|
2127
|
+
scannedItems
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
1883
2130
|
async getMetaItems(request) {
|
|
1884
2131
|
const { packageId } = request;
|
|
1885
2132
|
let items = [];
|
|
1886
2133
|
if (this.environmentId === void 0) {
|
|
1887
2134
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1888
2135
|
if (items.length === 0) {
|
|
1889
|
-
const alt =
|
|
2136
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1890
2137
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1891
2138
|
}
|
|
1892
2139
|
} else {
|
|
1893
2140
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1894
2141
|
if (items.length === 0) {
|
|
1895
|
-
const alt =
|
|
2142
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1896
2143
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1897
2144
|
}
|
|
1898
2145
|
}
|
|
@@ -1907,7 +2154,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1907
2154
|
if (packageId) whereClause._packageId = packageId;
|
|
1908
2155
|
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
1909
2156
|
if (!rs || rs.length === 0) {
|
|
1910
|
-
const alt =
|
|
2157
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1911
2158
|
if (alt) {
|
|
1912
2159
|
const altWhere = { type: alt, state: "active", organization_id: oid };
|
|
1913
2160
|
if (packageId) altWhere._packageId = packageId;
|
|
@@ -1974,28 +2221,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1974
2221
|
}
|
|
1975
2222
|
return {
|
|
1976
2223
|
type: request.type,
|
|
1977
|
-
items
|
|
2224
|
+
items: decorateMetadataItems(request.type, items)
|
|
1978
2225
|
};
|
|
1979
2226
|
}
|
|
1980
2227
|
async getMetaItem(request) {
|
|
1981
2228
|
let item;
|
|
1982
2229
|
const orgId = request.organizationId;
|
|
2230
|
+
const readState = request.state === "draft" ? "draft" : "active";
|
|
1983
2231
|
try {
|
|
1984
2232
|
const findOverlay = async (oid) => {
|
|
1985
2233
|
const where = {
|
|
1986
2234
|
type: request.type,
|
|
1987
2235
|
name: request.name,
|
|
1988
|
-
state:
|
|
2236
|
+
state: readState,
|
|
1989
2237
|
organization_id: oid
|
|
1990
2238
|
};
|
|
1991
2239
|
const rec = await this.engine.findOne("sys_metadata", { where });
|
|
1992
2240
|
if (rec) return rec;
|
|
1993
|
-
const alt =
|
|
2241
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
1994
2242
|
if (alt) {
|
|
1995
2243
|
const altWhere = {
|
|
1996
2244
|
type: alt,
|
|
1997
2245
|
name: request.name,
|
|
1998
|
-
state:
|
|
2246
|
+
state: readState,
|
|
1999
2247
|
organization_id: oid
|
|
2000
2248
|
};
|
|
2001
2249
|
return await this.engine.findOne("sys_metadata", { where: altWhere });
|
|
@@ -2008,6 +2256,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2008
2256
|
}
|
|
2009
2257
|
} catch {
|
|
2010
2258
|
}
|
|
2259
|
+
if (readState === "draft") {
|
|
2260
|
+
if (item === void 0) {
|
|
2261
|
+
const err = new Error(
|
|
2262
|
+
`[no_draft] No pending draft exists for ${request.type}/${request.name}.`
|
|
2263
|
+
);
|
|
2264
|
+
err.code = "no_draft";
|
|
2265
|
+
err.status = 404;
|
|
2266
|
+
throw err;
|
|
2267
|
+
}
|
|
2268
|
+
return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, item) };
|
|
2269
|
+
}
|
|
2011
2270
|
if (item === void 0) {
|
|
2012
2271
|
try {
|
|
2013
2272
|
const services = this.getServicesRegistry?.();
|
|
@@ -2017,7 +2276,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2017
2276
|
if (fromService !== void 0 && fromService !== null) {
|
|
2018
2277
|
item = fromService;
|
|
2019
2278
|
} else {
|
|
2020
|
-
const alt =
|
|
2279
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2021
2280
|
if (alt) {
|
|
2022
2281
|
const altFromService = await metadataService.get(alt, request.name);
|
|
2023
2282
|
if (altFromService !== void 0 && altFromService !== null) {
|
|
@@ -2032,14 +2291,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2032
2291
|
if (item === void 0) {
|
|
2033
2292
|
item = this.engine.registry.getItem(request.type, request.name);
|
|
2034
2293
|
if (item === void 0) {
|
|
2035
|
-
const alt =
|
|
2294
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2036
2295
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
2037
2296
|
}
|
|
2038
2297
|
}
|
|
2039
2298
|
return {
|
|
2040
2299
|
type: request.type,
|
|
2041
2300
|
name: request.name,
|
|
2042
|
-
item
|
|
2301
|
+
item: decorateMetadataItem(request.type, item)
|
|
2043
2302
|
};
|
|
2044
2303
|
}
|
|
2045
2304
|
/**
|
|
@@ -2065,7 +2324,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2065
2324
|
if (metadataService && typeof metadataService.get === "function") {
|
|
2066
2325
|
let fromService = await metadataService.get(request.type, request.name);
|
|
2067
2326
|
if (fromService === void 0 || fromService === null) {
|
|
2068
|
-
const alt =
|
|
2327
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2069
2328
|
if (alt) fromService = await metadataService.get(alt, request.name);
|
|
2070
2329
|
}
|
|
2071
2330
|
if (fromService !== void 0 && fromService !== null) code = fromService;
|
|
@@ -2075,7 +2334,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2075
2334
|
if (code === null) {
|
|
2076
2335
|
let regItem = this.engine.registry.getItem(request.type, request.name);
|
|
2077
2336
|
if (regItem === void 0) {
|
|
2078
|
-
const alt =
|
|
2337
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2079
2338
|
if (alt) regItem = this.engine.registry.getItem(alt, request.name);
|
|
2080
2339
|
}
|
|
2081
2340
|
if (regItem !== void 0) code = regItem;
|
|
@@ -2092,7 +2351,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2092
2351
|
};
|
|
2093
2352
|
let rec = await this.engine.findOne("sys_metadata", { where });
|
|
2094
2353
|
if (!rec) {
|
|
2095
|
-
const alt =
|
|
2354
|
+
const alt = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? import_shared3.SINGULAR_TO_PLURAL[request.type];
|
|
2096
2355
|
if (alt) {
|
|
2097
2356
|
rec = await this.engine.findOne("sys_metadata", {
|
|
2098
2357
|
where: { ...where, type: alt }
|
|
@@ -2118,13 +2377,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2118
2377
|
} catch {
|
|
2119
2378
|
}
|
|
2120
2379
|
const effective = overlay ?? code;
|
|
2380
|
+
const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
|
|
2121
2381
|
return {
|
|
2122
2382
|
type: request.type,
|
|
2123
2383
|
name: request.name,
|
|
2124
2384
|
code,
|
|
2125
2385
|
overlay,
|
|
2126
2386
|
overlayScope,
|
|
2127
|
-
effective
|
|
2387
|
+
effective,
|
|
2388
|
+
..._diagnostics ? { _diagnostics } : {}
|
|
2128
2389
|
};
|
|
2129
2390
|
}
|
|
2130
2391
|
async getUiView(request) {
|
|
@@ -3005,9 +3266,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3005
3266
|
for (const tok of raw.split(",")) {
|
|
3006
3267
|
const t = tok.trim();
|
|
3007
3268
|
if (!t) continue;
|
|
3008
|
-
const singular =
|
|
3269
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[t] ?? t;
|
|
3009
3270
|
set.add(singular);
|
|
3010
|
-
const plural =
|
|
3271
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[singular];
|
|
3011
3272
|
if (plural) set.add(plural);
|
|
3012
3273
|
}
|
|
3013
3274
|
this._envWritableTypes = set;
|
|
@@ -3019,13 +3280,49 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3019
3280
|
}
|
|
3020
3281
|
/** Normalize plural→singular before consulting the allow-list. */
|
|
3021
3282
|
static isOverlayAllowed(type) {
|
|
3022
|
-
const singular =
|
|
3283
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3023
3284
|
if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
|
|
3024
3285
|
return true;
|
|
3025
3286
|
}
|
|
3026
3287
|
const env = this.envWritableTypes();
|
|
3027
3288
|
return env.has(singular) || env.has(type);
|
|
3028
3289
|
}
|
|
3290
|
+
/** Does this type permit creating brand-new (artifact-free) items? */
|
|
3291
|
+
static isRuntimeCreateAllowed(type) {
|
|
3292
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3293
|
+
if (this.RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || this.RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
3294
|
+
return true;
|
|
3295
|
+
}
|
|
3296
|
+
if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) {
|
|
3297
|
+
return true;
|
|
3298
|
+
}
|
|
3299
|
+
return false;
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Does an artifact (npm-package-loaded) item exist at `(type, name)`?
|
|
3303
|
+
*
|
|
3304
|
+
* The schema registry's `_packageId` tag is set only when
|
|
3305
|
+
* `registerItem(..., packageId)` is called with a truthy packageId
|
|
3306
|
+
* — and only artifact loaders do that. DB-rehydrated items
|
|
3307
|
+
* (sys_metadata rows registered back into the registry by
|
|
3308
|
+
* `getMetaItems` / `loadMetaFromDb`) call `registerItem` without a
|
|
3309
|
+
* packageId, so they carry no `_packageId` and are correctly
|
|
3310
|
+
* excluded here.
|
|
3311
|
+
*
|
|
3312
|
+
* Used by the two-tier authorization model to distinguish
|
|
3313
|
+
* "overlaying a packaged item" (requires `allowOrgOverride`) from
|
|
3314
|
+
* "authoring a DB-only item" (requires only `allowRuntimeCreate`).
|
|
3315
|
+
*/
|
|
3316
|
+
isArtifactBacked(type, name) {
|
|
3317
|
+
const registry = this.engine?.registry;
|
|
3318
|
+
if (!registry || typeof registry.getItem !== "function") {
|
|
3319
|
+
return false;
|
|
3320
|
+
}
|
|
3321
|
+
const singular = import_shared3.PLURAL_TO_SINGULAR[type] ?? type;
|
|
3322
|
+
const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3323
|
+
if (!item || !item._packageId) return false;
|
|
3324
|
+
return item._packageId !== "sys_metadata";
|
|
3325
|
+
}
|
|
3029
3326
|
/**
|
|
3030
3327
|
* Mirror an object-type overlay write into the in-memory engine
|
|
3031
3328
|
* registry so subsequent CRUD finds the new schema. Idempotent and
|
|
@@ -3051,16 +3348,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3051
3348
|
if (!request.item) {
|
|
3052
3349
|
throw new Error("Item data is required");
|
|
3053
3350
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
const
|
|
3057
|
-
|
|
3058
|
-
);
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3351
|
+
const mode = request.mode === "draft" ? "draft" : "publish";
|
|
3352
|
+
if (this.environmentId !== void 0) {
|
|
3353
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
3354
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
3355
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
3356
|
+
if (artifactBacked && !overlayAllowed) {
|
|
3357
|
+
const err = new Error(
|
|
3358
|
+
`[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.`
|
|
3359
|
+
);
|
|
3360
|
+
err.code = "not_overridable";
|
|
3361
|
+
err.status = 403;
|
|
3362
|
+
throw err;
|
|
3363
|
+
}
|
|
3364
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
3365
|
+
const err = new Error(
|
|
3366
|
+
`[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.`
|
|
3367
|
+
);
|
|
3368
|
+
err.code = "not_creatable";
|
|
3369
|
+
err.status = 403;
|
|
3370
|
+
throw err;
|
|
3371
|
+
}
|
|
3062
3372
|
}
|
|
3063
|
-
const singularType =
|
|
3373
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3064
3374
|
if (!request.force && (singularType === "object" || singularType === "field")) {
|
|
3065
3375
|
try {
|
|
3066
3376
|
const existing = await this.getMetaItem({
|
|
@@ -3108,8 +3418,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3108
3418
|
}
|
|
3109
3419
|
}
|
|
3110
3420
|
await this.ensureOverlayIndex();
|
|
3111
|
-
const singularTypeForRepo =
|
|
3112
|
-
|
|
3421
|
+
const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3422
|
+
const overlayAllowedForRepo = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
3423
|
+
const runtimeCreateAllowedForRepo = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
3424
|
+
const useRepoPath = overlayAllowedForRepo || runtimeCreateAllowedForRepo;
|
|
3425
|
+
if (useRepoPath) {
|
|
3426
|
+
const artifactBacked = this.isArtifactBacked(singularTypeForRepo, request.name);
|
|
3427
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3113
3428
|
const orgId = request.organizationId ?? null;
|
|
3114
3429
|
const repo = this.getOverlayRepo(orgId);
|
|
3115
3430
|
const ref = {
|
|
@@ -3121,21 +3436,26 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3121
3436
|
if (request.parentVersion !== void 0) {
|
|
3122
3437
|
parentVersion = request.parentVersion;
|
|
3123
3438
|
} else {
|
|
3124
|
-
const current = await repo.get(ref);
|
|
3439
|
+
const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
|
|
3125
3440
|
parentVersion = current?.hash ?? null;
|
|
3126
3441
|
}
|
|
3127
3442
|
try {
|
|
3128
3443
|
const result = await repo.put(ref, request.item, {
|
|
3129
3444
|
parentVersion,
|
|
3130
3445
|
actor: request.actor ?? "system",
|
|
3131
|
-
source: "protocol.saveMetaItem"
|
|
3446
|
+
source: "protocol.saveMetaItem",
|
|
3447
|
+
intent,
|
|
3448
|
+
state: mode === "draft" ? "draft" : "active"
|
|
3132
3449
|
});
|
|
3133
|
-
|
|
3450
|
+
if (mode === "publish") {
|
|
3451
|
+
this.applyObjectRegistryMutation(request);
|
|
3452
|
+
}
|
|
3134
3453
|
return {
|
|
3135
3454
|
success: true,
|
|
3136
3455
|
version: result.version,
|
|
3137
3456
|
seq: result.seq,
|
|
3138
|
-
|
|
3457
|
+
state: mode === "draft" ? "draft" : "active",
|
|
3458
|
+
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}]`
|
|
3139
3459
|
};
|
|
3140
3460
|
} catch (err) {
|
|
3141
3461
|
if (err instanceof import_metadata_core2.ConflictError) {
|
|
@@ -3219,8 +3539,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3219
3539
|
* "no history" uniformly.
|
|
3220
3540
|
*/
|
|
3221
3541
|
async historyMetaItem(request) {
|
|
3222
|
-
const singularType =
|
|
3223
|
-
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
|
|
3542
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3543
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3224
3544
|
return { events: [] };
|
|
3225
3545
|
}
|
|
3226
3546
|
const orgId = request.organizationId ?? null;
|
|
@@ -3238,22 +3558,238 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3238
3558
|
return { events };
|
|
3239
3559
|
}
|
|
3240
3560
|
/**
|
|
3241
|
-
*
|
|
3242
|
-
*
|
|
3243
|
-
*
|
|
3244
|
-
* with {@link saveMetaItem}.
|
|
3561
|
+
* Promote the pending draft overlay to the live (`active`) row.
|
|
3562
|
+
* Records a history event with `op='publish'`. 404 (`[no_draft]`)
|
|
3563
|
+
* when there is nothing to publish.
|
|
3245
3564
|
*/
|
|
3246
|
-
async
|
|
3247
|
-
|
|
3565
|
+
async publishMetaItem(request) {
|
|
3566
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3567
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3248
3568
|
const err = new Error(
|
|
3249
|
-
`[not_overridable] Metadata type '${request.type}'
|
|
3569
|
+
`[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
|
|
3250
3570
|
);
|
|
3251
3571
|
err.code = "not_overridable";
|
|
3252
3572
|
err.status = 403;
|
|
3253
3573
|
throw err;
|
|
3254
3574
|
}
|
|
3255
|
-
|
|
3256
|
-
const
|
|
3575
|
+
await this.ensureOverlayIndex();
|
|
3576
|
+
const orgId = request.organizationId ?? null;
|
|
3577
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3578
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3579
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3580
|
+
const ref = {
|
|
3581
|
+
type: singularType,
|
|
3582
|
+
name: request.name,
|
|
3583
|
+
org: orgId ?? "env"
|
|
3584
|
+
};
|
|
3585
|
+
try {
|
|
3586
|
+
const result = await repo.promoteDraft(ref, {
|
|
3587
|
+
actor: request.actor ?? "system",
|
|
3588
|
+
source: "protocol.publishMetaItem",
|
|
3589
|
+
...request.message ? { message: request.message } : {},
|
|
3590
|
+
intent
|
|
3591
|
+
});
|
|
3592
|
+
this.applyObjectRegistryMutation({
|
|
3593
|
+
type: request.type,
|
|
3594
|
+
name: request.name,
|
|
3595
|
+
item: result.item.body
|
|
3596
|
+
});
|
|
3597
|
+
return {
|
|
3598
|
+
success: true,
|
|
3599
|
+
version: result.version,
|
|
3600
|
+
seq: result.seq,
|
|
3601
|
+
message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3602
|
+
};
|
|
3603
|
+
} catch (err) {
|
|
3604
|
+
if (err instanceof import_metadata_core2.ConflictError) {
|
|
3605
|
+
const conflict = new Error(
|
|
3606
|
+
`[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"}.`
|
|
3607
|
+
);
|
|
3608
|
+
conflict.code = "metadata_conflict";
|
|
3609
|
+
conflict.status = 409;
|
|
3610
|
+
conflict.expectedParent = err.expectedParent;
|
|
3611
|
+
conflict.actualHead = err.actualHead;
|
|
3612
|
+
throw conflict;
|
|
3613
|
+
}
|
|
3614
|
+
throw err;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Restore the body recorded at history `toVersion` as the new
|
|
3619
|
+
* live row. Writes a history event with `op='revert'`. 404
|
|
3620
|
+
* (`[version_not_found]`) when the target version doesn't exist;
|
|
3621
|
+
* 409 (`[version_not_restorable]`) when the target is a delete
|
|
3622
|
+
* tombstone (no body to bring back).
|
|
3623
|
+
*/
|
|
3624
|
+
async rollbackMetaItem(request) {
|
|
3625
|
+
if (!Number.isFinite(request.toVersion) || request.toVersion < 1) {
|
|
3626
|
+
const err = new Error(
|
|
3627
|
+
`[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`
|
|
3628
|
+
);
|
|
3629
|
+
err.code = "invalid_request";
|
|
3630
|
+
err.status = 400;
|
|
3631
|
+
throw err;
|
|
3632
|
+
}
|
|
3633
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3634
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3635
|
+
const err = new Error(
|
|
3636
|
+
`[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
|
|
3637
|
+
);
|
|
3638
|
+
err.code = "not_overridable";
|
|
3639
|
+
err.status = 403;
|
|
3640
|
+
throw err;
|
|
3641
|
+
}
|
|
3642
|
+
await this.ensureOverlayIndex();
|
|
3643
|
+
const orgId = request.organizationId ?? null;
|
|
3644
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3645
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3646
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3647
|
+
const ref = {
|
|
3648
|
+
type: singularType,
|
|
3649
|
+
name: request.name,
|
|
3650
|
+
org: orgId ?? "env"
|
|
3651
|
+
};
|
|
3652
|
+
try {
|
|
3653
|
+
const result = await repo.restoreVersion(ref, request.toVersion, {
|
|
3654
|
+
actor: request.actor ?? "system",
|
|
3655
|
+
source: "protocol.rollbackMetaItem",
|
|
3656
|
+
...request.message ? { message: request.message } : {},
|
|
3657
|
+
intent
|
|
3658
|
+
});
|
|
3659
|
+
this.applyObjectRegistryMutation({
|
|
3660
|
+
type: request.type,
|
|
3661
|
+
name: request.name,
|
|
3662
|
+
item: result.item.body
|
|
3663
|
+
});
|
|
3664
|
+
return {
|
|
3665
|
+
success: true,
|
|
3666
|
+
version: result.version,
|
|
3667
|
+
seq: result.seq,
|
|
3668
|
+
restoredFromVersion: request.toVersion,
|
|
3669
|
+
message: `Reverted to version ${request.toVersion} \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3670
|
+
};
|
|
3671
|
+
} catch (err) {
|
|
3672
|
+
if (err instanceof import_metadata_core2.ConflictError) {
|
|
3673
|
+
const conflict = new Error(
|
|
3674
|
+
`[metadata_conflict] ${request.type}/${request.name} advanced during rollback. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
|
|
3675
|
+
);
|
|
3676
|
+
conflict.code = "metadata_conflict";
|
|
3677
|
+
conflict.status = 409;
|
|
3678
|
+
conflict.expectedParent = err.expectedParent;
|
|
3679
|
+
conflict.actualHead = err.actualHead;
|
|
3680
|
+
throw conflict;
|
|
3681
|
+
}
|
|
3682
|
+
throw err;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* Compute a shallow structural diff between two historical
|
|
3687
|
+
* versions of a metadata item. Either side may be omitted: when
|
|
3688
|
+
* `toVersion` is undefined the current active body is used; when
|
|
3689
|
+
* `fromVersion` is undefined the immediately previous history row
|
|
3690
|
+
* is used. Returns `{ added, removed, changed }` keyed by JSON
|
|
3691
|
+
* pointer-style paths for primitive leaves; nested objects/arrays
|
|
3692
|
+
* are reported as a single change record.
|
|
3693
|
+
*/
|
|
3694
|
+
async diffMetaItem(request) {
|
|
3695
|
+
const singularType = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3696
|
+
const orgId = request.organizationId ?? null;
|
|
3697
|
+
const events = (await this.historyMetaItem({
|
|
3698
|
+
type: singularType,
|
|
3699
|
+
name: request.name,
|
|
3700
|
+
...orgId ? { organizationId: orgId } : {}
|
|
3701
|
+
})).events;
|
|
3702
|
+
const versions = events.map((ev) => ev.version).filter((v) => typeof v === "number");
|
|
3703
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3704
|
+
const fullRef = {
|
|
3705
|
+
type: singularType,
|
|
3706
|
+
name: request.name,
|
|
3707
|
+
org: orgId ?? "env"
|
|
3708
|
+
};
|
|
3709
|
+
const histRows = [];
|
|
3710
|
+
try {
|
|
3711
|
+
const engineAny = this.engine;
|
|
3712
|
+
const rows = await engineAny.find("sys_metadata_history", {
|
|
3713
|
+
where: {
|
|
3714
|
+
organization_id: orgId,
|
|
3715
|
+
type: singularType,
|
|
3716
|
+
name: request.name
|
|
3717
|
+
}
|
|
3718
|
+
});
|
|
3719
|
+
rows.sort((a, b) => (a.version ?? 0) - (b.version ?? 0));
|
|
3720
|
+
for (const r of rows) {
|
|
3721
|
+
const body = r.metadata == null ? null : typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
3722
|
+
histRows.push({ version: r.version ?? 0, body });
|
|
3723
|
+
}
|
|
3724
|
+
} catch {
|
|
3725
|
+
}
|
|
3726
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
3727
|
+
for (const r of histRows) byVersion.set(r.version, r.body);
|
|
3728
|
+
let fromBody = null;
|
|
3729
|
+
let toBody = null;
|
|
3730
|
+
let fromVersion = null;
|
|
3731
|
+
let toVersion = null;
|
|
3732
|
+
if (request.toVersion !== void 0) {
|
|
3733
|
+
toVersion = request.toVersion;
|
|
3734
|
+
toBody = byVersion.get(request.toVersion) ?? null;
|
|
3735
|
+
} else {
|
|
3736
|
+
const current = await repo.get(fullRef, { state: "active" });
|
|
3737
|
+
toBody = current ? current.body : null;
|
|
3738
|
+
toVersion = histRows.length ? histRows[histRows.length - 1].version : null;
|
|
3739
|
+
}
|
|
3740
|
+
if (request.fromVersion !== void 0) {
|
|
3741
|
+
fromVersion = request.fromVersion;
|
|
3742
|
+
fromBody = byVersion.get(request.fromVersion) ?? null;
|
|
3743
|
+
} else if (toVersion !== null) {
|
|
3744
|
+
const sorted = histRows.map((r) => r.version).filter((v) => v < toVersion);
|
|
3745
|
+
if (sorted.length) {
|
|
3746
|
+
fromVersion = sorted[sorted.length - 1];
|
|
3747
|
+
fromBody = byVersion.get(fromVersion) ?? null;
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
const diff = diffShallow(fromBody ?? {}, toBody ?? {});
|
|
3751
|
+
const _used = versions;
|
|
3752
|
+
void _used;
|
|
3753
|
+
return {
|
|
3754
|
+
type: request.type,
|
|
3755
|
+
name: request.name,
|
|
3756
|
+
fromVersion,
|
|
3757
|
+
toVersion,
|
|
3758
|
+
...diff
|
|
3759
|
+
};
|
|
3760
|
+
}
|
|
3761
|
+
/**
|
|
3762
|
+
* Remove a customization overlay row for the given metadata item, so the
|
|
3763
|
+
* next read falls through to the artifact-loaded default. Implements the
|
|
3764
|
+
* "Reset to factory default" semantic from ADR-0005. Whitelist is shared
|
|
3765
|
+
* with {@link saveMetaItem}.
|
|
3766
|
+
*/
|
|
3767
|
+
async deleteMetaItem(request) {
|
|
3768
|
+
if (this.environmentId !== void 0) {
|
|
3769
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
3770
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
3771
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
3772
|
+
if (artifactBacked && !overlayAllowed) {
|
|
3773
|
+
const err = new Error(
|
|
3774
|
+
`[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.`
|
|
3775
|
+
);
|
|
3776
|
+
err.code = "not_overridable";
|
|
3777
|
+
err.status = 403;
|
|
3778
|
+
throw err;
|
|
3779
|
+
}
|
|
3780
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
3781
|
+
const err = new Error(
|
|
3782
|
+
`[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.`
|
|
3783
|
+
);
|
|
3784
|
+
err.code = "not_creatable";
|
|
3785
|
+
err.status = 403;
|
|
3786
|
+
throw err;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
const singularTypeForRepo = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3790
|
+
const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
3791
|
+
const runtimeCreateAllowedForRepoDel = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
3792
|
+
const useRepoPath = overlayAllowedForRepoDel || runtimeCreateAllowedForRepoDel;
|
|
3257
3793
|
if (useRepoPath) {
|
|
3258
3794
|
const orgId = request.organizationId ?? null;
|
|
3259
3795
|
const repo = this.getOverlayRepo(orgId);
|
|
@@ -3263,19 +3799,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3263
3799
|
org: orgId ?? "env"
|
|
3264
3800
|
};
|
|
3265
3801
|
try {
|
|
3266
|
-
const
|
|
3802
|
+
const targetState = request.state === "draft" ? "draft" : "active";
|
|
3803
|
+
const current = await repo.get(ref, { state: targetState });
|
|
3267
3804
|
if (!current) {
|
|
3268
3805
|
return {
|
|
3269
3806
|
success: true,
|
|
3270
3807
|
reset: false,
|
|
3271
|
-
message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
|
|
3808
|
+
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.`
|
|
3272
3809
|
};
|
|
3273
3810
|
}
|
|
3274
3811
|
const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
|
|
3275
3812
|
const result = await repo.delete(ref, {
|
|
3276
3813
|
parentVersion,
|
|
3277
3814
|
actor: request.actor ?? "system",
|
|
3278
|
-
source: "protocol.deleteMetaItem"
|
|
3815
|
+
source: "protocol.deleteMetaItem",
|
|
3816
|
+
intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
|
|
3817
|
+
state: targetState
|
|
3279
3818
|
});
|
|
3280
3819
|
if (this.environmentId === void 0) {
|
|
3281
3820
|
try {
|
|
@@ -3294,7 +3833,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3294
3833
|
success: true,
|
|
3295
3834
|
reset: true,
|
|
3296
3835
|
seq: result.seq,
|
|
3297
|
-
message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
|
|
3836
|
+
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}]`
|
|
3298
3837
|
};
|
|
3299
3838
|
} catch (err) {
|
|
3300
3839
|
if (err instanceof import_metadata_core2.ConflictError) {
|
|
@@ -3372,7 +3911,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3372
3911
|
for (const record of records) {
|
|
3373
3912
|
try {
|
|
3374
3913
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
3375
|
-
const normalizedType =
|
|
3914
|
+
const normalizedType = import_shared3.PLURAL_TO_SINGULAR[record.type] ?? record.type;
|
|
3376
3915
|
if (normalizedType === "object") {
|
|
3377
3916
|
this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
|
|
3378
3917
|
} else {
|
|
@@ -3406,7 +3945,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3406
3945
|
* — the engine never throws.
|
|
3407
3946
|
*/
|
|
3408
3947
|
async findReferencesToMeta(request) {
|
|
3409
|
-
const singularTarget =
|
|
3948
|
+
const singularTarget = import_shared3.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
3410
3949
|
const targetName = request.name;
|
|
3411
3950
|
const matchers = REFERENCE_PATHS[singularTarget];
|
|
3412
3951
|
if (!matchers || matchers.length === 0) {
|
|
@@ -3597,10 +4136,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3597
4136
|
*/
|
|
3598
4137
|
_ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
3599
4138
|
const out = /* @__PURE__ */ new Set();
|
|
3600
|
-
for (const entry of
|
|
4139
|
+
for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
|
|
3601
4140
|
if (!entry.allowOrgOverride) continue;
|
|
3602
4141
|
out.add(entry.type);
|
|
3603
|
-
const plural =
|
|
4142
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
|
|
3604
4143
|
if (plural) out.add(plural);
|
|
3605
4144
|
}
|
|
3606
4145
|
return out;
|
|
@@ -3617,13 +4156,48 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
|
3617
4156
|
* {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
|
|
3618
4157
|
*/
|
|
3619
4158
|
_ObjectStackProtocolImplementation._envWritableTypes = null;
|
|
4159
|
+
/**
|
|
4160
|
+
* Types that opt into runtime creation of brand-new items (ADR-0005
|
|
4161
|
+
* extension — two-tier model). A type may have
|
|
4162
|
+
* `allowOrgOverride: false` (cannot overlay artifact-shipped items)
|
|
4163
|
+
* yet still set `allowRuntimeCreate: true` (users can author new
|
|
4164
|
+
* items in `sys_metadata`). The two flags are orthogonal; see
|
|
4165
|
+
* {@link isArtifactBacked} for how the protocol decides which gate
|
|
4166
|
+
* applies to a given save/delete.
|
|
4167
|
+
*/
|
|
4168
|
+
/**
|
|
4169
|
+
* Set of type names that have a static entry in
|
|
4170
|
+
* `DEFAULT_METADATA_TYPE_REGISTRY`. Anything outside this set is
|
|
4171
|
+
* runtime-registered (plugin-provided types like `theme`, `api`,
|
|
4172
|
+
* `connector`) — the listing endpoint at `getMetaTypes()` synthesises
|
|
4173
|
+
* those with `allowRuntimeCreate: true`, so this gate must agree.
|
|
4174
|
+
*/
|
|
4175
|
+
_ObjectStackProtocolImplementation.STATIC_REGISTRY_TYPES = (() => {
|
|
4176
|
+
const out = /* @__PURE__ */ new Set();
|
|
4177
|
+
for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
|
|
4178
|
+
out.add(entry.type);
|
|
4179
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
|
|
4180
|
+
if (plural) out.add(plural);
|
|
4181
|
+
}
|
|
4182
|
+
return out;
|
|
4183
|
+
})();
|
|
4184
|
+
_ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
|
|
4185
|
+
const out = /* @__PURE__ */ new Set();
|
|
4186
|
+
for (const entry of import_kernel4.DEFAULT_METADATA_TYPE_REGISTRY) {
|
|
4187
|
+
if (!entry.allowRuntimeCreate) continue;
|
|
4188
|
+
out.add(entry.type);
|
|
4189
|
+
const plural = import_shared3.SINGULAR_TO_PLURAL[entry.type];
|
|
4190
|
+
if (plural) out.add(plural);
|
|
4191
|
+
}
|
|
4192
|
+
return out;
|
|
4193
|
+
})();
|
|
3620
4194
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
3621
4195
|
|
|
3622
4196
|
// src/engine.ts
|
|
3623
|
-
var
|
|
4197
|
+
var import_kernel5 = require("@objectstack/spec/kernel");
|
|
3624
4198
|
var import_core = require("@objectstack/core");
|
|
3625
4199
|
var import_system2 = require("@objectstack/spec/system");
|
|
3626
|
-
var
|
|
4200
|
+
var import_shared4 = require("@objectstack/spec/shared");
|
|
3627
4201
|
var import_formula2 = require("@objectstack/formula");
|
|
3628
4202
|
|
|
3629
4203
|
// src/hook-wrappers.ts
|
|
@@ -4894,9 +5468,9 @@ var _ObjectQL = class _ObjectQL {
|
|
|
4894
5468
|
const itemName = resolveMetadataItemName(key, item);
|
|
4895
5469
|
if (itemName) {
|
|
4896
5470
|
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
4897
|
-
this._registry.registerItem((0,
|
|
5471
|
+
this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", id);
|
|
4898
5472
|
} else {
|
|
4899
|
-
this.logger.warn(`Skipping ${(0,
|
|
5473
|
+
this.logger.warn(`Skipping ${(0, import_shared4.pluralToSingular)(key)} without a derivable name`, { id });
|
|
4900
5474
|
}
|
|
4901
5475
|
}
|
|
4902
5476
|
}
|
|
@@ -5023,7 +5597,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5023
5597
|
const itemName = resolveMetadataItemName(key, item);
|
|
5024
5598
|
if (itemName) {
|
|
5025
5599
|
const toRegister = item.name === itemName ? item : { ...item, name: itemName };
|
|
5026
|
-
this._registry.registerItem((0,
|
|
5600
|
+
this._registry.registerItem((0, import_shared4.pluralToSingular)(key), toRegister, "name", ownerId);
|
|
5027
5601
|
}
|
|
5028
5602
|
}
|
|
5029
5603
|
}
|
|
@@ -5909,7 +6483,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
5909
6483
|
*/
|
|
5910
6484
|
createContext(ctx) {
|
|
5911
6485
|
return new ScopedContext(
|
|
5912
|
-
|
|
6486
|
+
import_kernel5.ExecutionContextSchema.parse(ctx),
|
|
5913
6487
|
this
|
|
5914
6488
|
);
|
|
5915
6489
|
}
|