@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.mjs
CHANGED
|
@@ -390,6 +390,14 @@ var SchemaRegistry = class {
|
|
|
390
390
|
if (collection.has(storageKey)) {
|
|
391
391
|
this.log(`[Registry] Overwriting ${type}: ${storageKey}`);
|
|
392
392
|
}
|
|
393
|
+
if (packageId && collection.has(baseName)) {
|
|
394
|
+
const dbOnly = collection.get(baseName);
|
|
395
|
+
if (dbOnly && !dbOnly._packageId) {
|
|
396
|
+
console.warn(
|
|
397
|
+
`[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.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
393
401
|
collection.set(storageKey, item);
|
|
394
402
|
this.log(`[Registry] Registered ${type}: ${storageKey}`);
|
|
395
403
|
}
|
|
@@ -446,7 +454,9 @@ var SchemaRegistry = class {
|
|
|
446
454
|
const direct = collection.get(name);
|
|
447
455
|
if (direct) return direct;
|
|
448
456
|
for (const [key, item] of collection) {
|
|
449
|
-
if (key.endsWith(`:${name}`))
|
|
457
|
+
if (key.endsWith(`:${name}`)) {
|
|
458
|
+
return item;
|
|
459
|
+
}
|
|
450
460
|
}
|
|
451
461
|
return void 0;
|
|
452
462
|
}
|
|
@@ -626,6 +636,12 @@ import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from "@objectstack/spec/shared
|
|
|
626
636
|
var OVERLAY_ALLOWED_TYPES = new Set(
|
|
627
637
|
DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
|
|
628
638
|
);
|
|
639
|
+
var STATIC_REGISTRY_TYPES = new Set(
|
|
640
|
+
DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)
|
|
641
|
+
);
|
|
642
|
+
var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
|
|
643
|
+
DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type)
|
|
644
|
+
);
|
|
629
645
|
var _envWritableMetadataTypes = null;
|
|
630
646
|
function envWritableMetadataTypes() {
|
|
631
647
|
if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
|
|
@@ -675,11 +691,16 @@ var SysMetadataRepository = class {
|
|
|
675
691
|
/**
|
|
676
692
|
* Read the current overlay row. Returns null if no row exists —
|
|
677
693
|
* callers (e.g. LayeredRepository) fall through to lower layers.
|
|
694
|
+
*
|
|
695
|
+
* `opts.state` selects which lifecycle row to read: defaults to the
|
|
696
|
+
* live published row (`'active'`). Pass `'draft'` to read the pending
|
|
697
|
+
* unpublished revision (if any).
|
|
678
698
|
*/
|
|
679
|
-
async get(ref) {
|
|
699
|
+
async get(ref, opts) {
|
|
680
700
|
this.assertOpen();
|
|
701
|
+
const state = opts?.state ?? "active";
|
|
681
702
|
const row = await this.engine.findOne("sys_metadata", {
|
|
682
|
-
where: this.whereFor(ref)
|
|
703
|
+
where: this.whereFor(ref, state)
|
|
683
704
|
});
|
|
684
705
|
if (!row) return null;
|
|
685
706
|
return this.rowToItem(ref, row);
|
|
@@ -722,12 +743,13 @@ var SysMetadataRepository = class {
|
|
|
722
743
|
}
|
|
723
744
|
async put(ref, spec, opts) {
|
|
724
745
|
this.assertOpen();
|
|
725
|
-
this.assertAllowed(ref.type);
|
|
746
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
747
|
+
const state = opts.state ?? "active";
|
|
726
748
|
const body = spec ?? {};
|
|
727
749
|
const hash = hashSpec(body);
|
|
728
750
|
const result = await this.withTxn(async (ctx) => {
|
|
729
751
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
730
|
-
where: this.whereFor(ref),
|
|
752
|
+
where: this.whereFor(ref, state),
|
|
731
753
|
context: ctx
|
|
732
754
|
});
|
|
733
755
|
const existingHash = existing?.checksum ?? null;
|
|
@@ -739,7 +761,8 @@ var SysMetadataRepository = class {
|
|
|
739
761
|
return { skipped: true, version: hash, seq: item2.seq, item: item2 };
|
|
740
762
|
}
|
|
741
763
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
742
|
-
const
|
|
764
|
+
const baseOp = existing ? "update" : "create";
|
|
765
|
+
const op = opts.opType ?? baseOp;
|
|
743
766
|
const version = await this.nextItemVersion(ref, ctx);
|
|
744
767
|
const eventSeq = await this.nextEventSeq(ctx);
|
|
745
768
|
const parentRowData = {
|
|
@@ -748,7 +771,7 @@ var SysMetadataRepository = class {
|
|
|
748
771
|
organization_id: this.organizationId,
|
|
749
772
|
metadata: JSON.stringify(body),
|
|
750
773
|
checksum: hash,
|
|
751
|
-
state
|
|
774
|
+
state,
|
|
752
775
|
version,
|
|
753
776
|
updated_at: now
|
|
754
777
|
};
|
|
@@ -814,25 +837,28 @@ var SysMetadataRepository = class {
|
|
|
814
837
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
815
838
|
}
|
|
816
839
|
this.seqCounter = result.seq;
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
840
|
+
if (state === "active") {
|
|
841
|
+
this.broadcast({
|
|
842
|
+
seq: result.seq,
|
|
843
|
+
op: result.op,
|
|
844
|
+
ref: this.fullRef(ref),
|
|
845
|
+
hash: result.version,
|
|
846
|
+
parentHash: result.existingHash,
|
|
847
|
+
actor: result.actor,
|
|
848
|
+
message: result.message,
|
|
849
|
+
ts: result.now,
|
|
850
|
+
source: result.source
|
|
851
|
+
});
|
|
852
|
+
}
|
|
828
853
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
829
854
|
}
|
|
830
855
|
async delete(ref, opts) {
|
|
831
856
|
this.assertOpen();
|
|
832
|
-
this.assertAllowed(ref.type);
|
|
857
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
858
|
+
const state = opts.state ?? "active";
|
|
833
859
|
const result = await this.withTxn(async (ctx) => {
|
|
834
860
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
835
|
-
where: this.whereFor(ref),
|
|
861
|
+
where: this.whereFor(ref, state),
|
|
836
862
|
context: ctx
|
|
837
863
|
});
|
|
838
864
|
if (!existing) {
|
|
@@ -849,32 +875,38 @@ var SysMetadataRepository = class {
|
|
|
849
875
|
);
|
|
850
876
|
}
|
|
851
877
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
852
|
-
|
|
853
|
-
|
|
878
|
+
let version = 0;
|
|
879
|
+
let eventSeq = 0;
|
|
880
|
+
if (state === "active") {
|
|
881
|
+
version = await this.nextItemVersion(ref, ctx);
|
|
882
|
+
eventSeq = await this.nextEventSeq(ctx);
|
|
883
|
+
}
|
|
854
884
|
await this.engine.delete("sys_metadata", {
|
|
855
885
|
where: { id: existingId },
|
|
856
886
|
context: ctx
|
|
857
887
|
});
|
|
858
|
-
|
|
859
|
-
this.
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
888
|
+
if (state === "active") {
|
|
889
|
+
await this.engine.insert(
|
|
890
|
+
this.historyTable,
|
|
891
|
+
{
|
|
892
|
+
id: this.uuid(),
|
|
893
|
+
event_seq: eventSeq,
|
|
894
|
+
type: ref.type,
|
|
895
|
+
name: ref.name,
|
|
896
|
+
version,
|
|
897
|
+
operation_type: "delete",
|
|
898
|
+
metadata: null,
|
|
899
|
+
checksum: null,
|
|
900
|
+
previous_checksum: existingHash,
|
|
901
|
+
change_note: opts.message,
|
|
902
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
903
|
+
organization_id: this.organizationId,
|
|
904
|
+
recorded_by: opts.actor,
|
|
905
|
+
recorded_at: now
|
|
906
|
+
},
|
|
907
|
+
{ context: ctx }
|
|
908
|
+
);
|
|
909
|
+
}
|
|
878
910
|
return {
|
|
879
911
|
eventSeq,
|
|
880
912
|
existingHash,
|
|
@@ -884,20 +916,117 @@ var SysMetadataRepository = class {
|
|
|
884
916
|
actor: opts.actor
|
|
885
917
|
};
|
|
886
918
|
});
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
919
|
+
if (state === "active") {
|
|
920
|
+
this.seqCounter = result.eventSeq;
|
|
921
|
+
this.broadcast({
|
|
922
|
+
seq: result.eventSeq,
|
|
923
|
+
op: "delete",
|
|
924
|
+
ref: this.fullRef(ref),
|
|
925
|
+
hash: null,
|
|
926
|
+
parentHash: result.existingHash,
|
|
927
|
+
actor: result.actor,
|
|
928
|
+
message: result.message,
|
|
929
|
+
ts: result.now,
|
|
930
|
+
source: result.source
|
|
931
|
+
});
|
|
932
|
+
}
|
|
899
933
|
return { seq: result.eventSeq };
|
|
900
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* Promote the pending draft row for `ref` into the live (`active`)
|
|
937
|
+
* overlay. Atomic: reads the draft inside the same transaction, runs
|
|
938
|
+
* the canonical `put` to upsert the active row (which appends a
|
|
939
|
+
* history event with `operation_type='publish'`), then deletes the
|
|
940
|
+
* draft row.
|
|
941
|
+
*
|
|
942
|
+
* Errors if no draft exists (callers should 404). The active row's
|
|
943
|
+
* `parentVersion` is computed from the current active hash so this
|
|
944
|
+
* also surfaces optimistic-lock conflicts when something else has
|
|
945
|
+
* published in between (e.g. another admin reverted to an older
|
|
946
|
+
* version since the draft was authored).
|
|
947
|
+
*/
|
|
948
|
+
async promoteDraft(ref, opts) {
|
|
949
|
+
this.assertOpen();
|
|
950
|
+
const draft = await this.get(ref, { state: "draft" });
|
|
951
|
+
if (!draft) {
|
|
952
|
+
const err = new Error(
|
|
953
|
+
`[no_draft] No pending draft exists for ${ref.type}/${ref.name} \u2014 nothing to publish.`
|
|
954
|
+
);
|
|
955
|
+
err.code = "no_draft";
|
|
956
|
+
err.status = 404;
|
|
957
|
+
throw err;
|
|
958
|
+
}
|
|
959
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
960
|
+
const result = await this.put(ref, draft.body, {
|
|
961
|
+
parentVersion: currentActive?.hash ?? null,
|
|
962
|
+
actor: opts.actor,
|
|
963
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
964
|
+
message: opts.message ?? `publish draft (hash ${draft.hash})`,
|
|
965
|
+
intent: opts.intent ?? "override-artifact",
|
|
966
|
+
state: "active",
|
|
967
|
+
opType: "publish"
|
|
968
|
+
});
|
|
969
|
+
try {
|
|
970
|
+
await this.delete(ref, {
|
|
971
|
+
parentVersion: draft.hash,
|
|
972
|
+
actor: opts.actor,
|
|
973
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
974
|
+
intent: opts.intent ?? "override-artifact",
|
|
975
|
+
state: "draft"
|
|
976
|
+
});
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
return result;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Restore the body recorded in history at `targetVersion` (per-org
|
|
983
|
+
* lineage counter) as the new active row. Writes a history event
|
|
984
|
+
* with `operation_type='revert'` so the audit trail captures the
|
|
985
|
+
* intent. Does NOT touch any draft row.
|
|
986
|
+
*
|
|
987
|
+
* Throws `[version_not_found]` (404) if the target version row is
|
|
988
|
+
* missing or is a delete tombstone (no body to restore).
|
|
989
|
+
*/
|
|
990
|
+
async restoreVersion(ref, targetVersion, opts) {
|
|
991
|
+
this.assertOpen();
|
|
992
|
+
const full = this.fullRef(ref);
|
|
993
|
+
const row = await this.engine.findOne(this.historyTable, {
|
|
994
|
+
where: {
|
|
995
|
+
organization_id: this.organizationId,
|
|
996
|
+
type: full.type,
|
|
997
|
+
name: full.name,
|
|
998
|
+
version: targetVersion
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
if (!row) {
|
|
1002
|
+
const err = new Error(
|
|
1003
|
+
`[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`
|
|
1004
|
+
);
|
|
1005
|
+
err.code = "version_not_found";
|
|
1006
|
+
err.status = 404;
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
const raw = row.metadata;
|
|
1010
|
+
if (raw === null || raw === void 0) {
|
|
1011
|
+
const err = new Error(
|
|
1012
|
+
`[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone \u2014 nothing to restore.`
|
|
1013
|
+
);
|
|
1014
|
+
err.code = "version_not_restorable";
|
|
1015
|
+
err.status = 409;
|
|
1016
|
+
throw err;
|
|
1017
|
+
}
|
|
1018
|
+
const body = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1019
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
1020
|
+
return this.put(ref, body, {
|
|
1021
|
+
parentVersion: currentActive?.hash ?? null,
|
|
1022
|
+
actor: opts.actor,
|
|
1023
|
+
source: opts.source ?? "sys-metadata-repo.revert",
|
|
1024
|
+
message: opts.message ?? `revert to version ${targetVersion}`,
|
|
1025
|
+
intent: opts.intent ?? "override-artifact",
|
|
1026
|
+
state: "active",
|
|
1027
|
+
opType: "revert"
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
901
1030
|
async *list(filter) {
|
|
902
1031
|
this.assertOpen();
|
|
903
1032
|
const where = {
|
|
@@ -952,6 +1081,7 @@ var SysMetadataRepository = class {
|
|
|
952
1081
|
ref: full,
|
|
953
1082
|
hash: row.checksum ?? null,
|
|
954
1083
|
parentHash: row.previous_checksum ?? null,
|
|
1084
|
+
version: typeof row.version === "number" ? row.version : void 0,
|
|
955
1085
|
actor: row.recorded_by ?? "unknown",
|
|
956
1086
|
message: row.change_note ?? void 0,
|
|
957
1087
|
ts: row.recorded_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
@@ -1033,29 +1163,52 @@ var SysMetadataRepository = class {
|
|
|
1033
1163
|
assertOpen() {
|
|
1034
1164
|
if (this.closed) throw new Error("SysMetadataRepository is closed");
|
|
1035
1165
|
}
|
|
1036
|
-
|
|
1166
|
+
/**
|
|
1167
|
+
* Defense-in-depth authorization gate.
|
|
1168
|
+
*
|
|
1169
|
+
* `intent` defaults to `'override-artifact'` (the historical strict
|
|
1170
|
+
* behavior). The protocol layer passes `'runtime-only'` after it has
|
|
1171
|
+
* verified — via the schema registry — that no artifact item exists
|
|
1172
|
+
* at `(type, name)`. In that case we accept types with
|
|
1173
|
+
* `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
|
|
1174
|
+
*
|
|
1175
|
+
* The env-var escape hatch (`OBJECTSTACK_METADATA_WRITABLE`) still
|
|
1176
|
+
* applies to BOTH intents, so operators can opt into artifact
|
|
1177
|
+
* overrides at runtime for emergency fixes.
|
|
1178
|
+
*/
|
|
1179
|
+
assertAllowed(type, intent = "override-artifact") {
|
|
1037
1180
|
const singular = PLURAL_TO_SINGULAR[type] ?? type;
|
|
1038
1181
|
const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
|
|
1039
1182
|
if (allowedByRegistry) return;
|
|
1183
|
+
if (intent === "runtime-only") {
|
|
1184
|
+
if (RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
if (!STATIC_REGISTRY_TYPES.has(singular) && !STATIC_REGISTRY_TYPES.has(type)) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1040
1191
|
const env = envWritableMetadataTypes();
|
|
1041
1192
|
if (env.has(singular) || env.has(type)) return;
|
|
1042
1193
|
const allowed = [
|
|
1043
1194
|
...OVERLAY_ALLOWED_TYPES,
|
|
1044
1195
|
...envWritableMetadataTypes()
|
|
1045
1196
|
];
|
|
1197
|
+
const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
|
|
1198
|
+
const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
|
|
1046
1199
|
const err = new Error(
|
|
1047
|
-
`[
|
|
1200
|
+
`[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
|
|
1048
1201
|
);
|
|
1049
|
-
err.code =
|
|
1202
|
+
err.code = code;
|
|
1050
1203
|
err.status = 403;
|
|
1051
1204
|
throw err;
|
|
1052
1205
|
}
|
|
1053
|
-
whereFor(ref) {
|
|
1206
|
+
whereFor(ref, state = "active") {
|
|
1054
1207
|
return {
|
|
1055
1208
|
type: ref.type,
|
|
1056
1209
|
name: ref.name,
|
|
1057
1210
|
organization_id: this.organizationId,
|
|
1058
|
-
state
|
|
1211
|
+
state
|
|
1059
1212
|
};
|
|
1060
1213
|
}
|
|
1061
1214
|
fullRef(ref) {
|
|
@@ -1151,37 +1304,64 @@ var SysMetadataRepository = class {
|
|
|
1151
1304
|
|
|
1152
1305
|
// src/protocol.ts
|
|
1153
1306
|
import { ConflictError as ConflictError2 } from "@objectstack/metadata-core";
|
|
1154
|
-
import { parseFilterAST, isFilterAST
|
|
1155
|
-
import { PLURAL_TO_SINGULAR as
|
|
1156
|
-
import {
|
|
1157
|
-
import {
|
|
1158
|
-
import {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1307
|
+
import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
|
|
1308
|
+
import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR3, SINGULAR_TO_PLURAL as SINGULAR_TO_PLURAL2 } from "@objectstack/spec/shared";
|
|
1309
|
+
import { METADATA_FORM_REGISTRY } from "@objectstack/spec/system";
|
|
1310
|
+
import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2 } from "@objectstack/spec/kernel";
|
|
1311
|
+
import {
|
|
1312
|
+
extractProtection,
|
|
1313
|
+
evaluateLockForWrite,
|
|
1314
|
+
evaluateLockForDelete,
|
|
1315
|
+
resolveLockState
|
|
1316
|
+
} from "@objectstack/spec/kernel";
|
|
1163
1317
|
import { z } from "zod";
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1318
|
+
|
|
1319
|
+
// src/metadata-diagnostics.ts
|
|
1320
|
+
import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
|
|
1321
|
+
import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR2 } from "@objectstack/spec/shared";
|
|
1322
|
+
function computeMetadataDiagnostics(type, item) {
|
|
1323
|
+
const singular = PLURAL_TO_SINGULAR2[type] ?? type;
|
|
1324
|
+
const schema = getMetadataTypeSchema(singular);
|
|
1325
|
+
if (!schema) return void 0;
|
|
1326
|
+
if (item === null || item === void 0 || typeof item !== "object") {
|
|
1327
|
+
return {
|
|
1328
|
+
valid: false,
|
|
1329
|
+
errors: [{
|
|
1330
|
+
path: "",
|
|
1331
|
+
message: "Metadata document must be a non-null object",
|
|
1332
|
+
code: "invalid_type"
|
|
1333
|
+
}]
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
const candidate = "_diagnostics" in item ? stripDiagnostics(item) : item;
|
|
1337
|
+
const parsed = schema.safeParse(candidate);
|
|
1338
|
+
if (parsed.success) {
|
|
1339
|
+
return { valid: true };
|
|
1340
|
+
}
|
|
1341
|
+
const errors = parsed.error.issues.map((issue) => ({
|
|
1342
|
+
path: issue.path.map(String).join("."),
|
|
1343
|
+
message: issue.message,
|
|
1344
|
+
code: issue.code
|
|
1345
|
+
}));
|
|
1346
|
+
return { valid: false, errors };
|
|
1347
|
+
}
|
|
1348
|
+
function stripDiagnostics(item) {
|
|
1349
|
+
const { _diagnostics: _drop, ...rest } = item;
|
|
1350
|
+
void _drop;
|
|
1351
|
+
return rest;
|
|
1352
|
+
}
|
|
1353
|
+
function decorateMetadataItem(type, item) {
|
|
1354
|
+
if (!item || typeof item !== "object") return item;
|
|
1355
|
+
const diagnostics = computeMetadataDiagnostics(type, item);
|
|
1356
|
+
if (!diagnostics) return item;
|
|
1357
|
+
return { ...item, _diagnostics: diagnostics };
|
|
1358
|
+
}
|
|
1359
|
+
function decorateMetadataItems(type, items) {
|
|
1360
|
+
if (!Array.isArray(items)) return items;
|
|
1361
|
+
return items.map((item) => decorateMetadataItem(type, item));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/protocol.ts
|
|
1185
1365
|
var TYPE_TO_FORM = METADATA_FORM_REGISTRY;
|
|
1186
1366
|
var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
1187
1367
|
function toJsonSchemaSafe(schema) {
|
|
@@ -1211,9 +1391,17 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1211
1391
|
abstract: { type: "boolean", default: false },
|
|
1212
1392
|
datasource: { type: "string" },
|
|
1213
1393
|
fields: {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1394
|
+
// Canonical Object.fields is a name-keyed map
|
|
1395
|
+
// (Record<string, FieldDefinition>) — insertion order is
|
|
1396
|
+
// display order. The SchemaForm engine recognises
|
|
1397
|
+
// `additionalProperties` as a Record and dispatches to
|
|
1398
|
+
// the `record` form-field renderer (ADR-0007). The form
|
|
1399
|
+
// layout in `object.form.ts` declares `type: 'record'`
|
|
1400
|
+
// so the inner `additionalProperties` schema is used to
|
|
1401
|
+
// shape each value.
|
|
1402
|
+
type: "object",
|
|
1403
|
+
default: {},
|
|
1404
|
+
additionalProperties: {
|
|
1217
1405
|
type: "object",
|
|
1218
1406
|
properties: {
|
|
1219
1407
|
name: { type: "string" },
|
|
@@ -1224,7 +1412,7 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1224
1412
|
defaultValue: {},
|
|
1225
1413
|
description: { type: "string" }
|
|
1226
1414
|
},
|
|
1227
|
-
required: ["
|
|
1415
|
+
required: ["type"]
|
|
1228
1416
|
}
|
|
1229
1417
|
},
|
|
1230
1418
|
capabilities: { type: "object", additionalProperties: true }
|
|
@@ -1369,19 +1557,22 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1369
1557
|
additionalProperties: true
|
|
1370
1558
|
}
|
|
1371
1559
|
};
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1560
|
+
function resolveOverlaySchema(type, _item) {
|
|
1561
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
1562
|
+
return getMetadataTypeSchema2(singular) ?? null;
|
|
1563
|
+
}
|
|
1564
|
+
function mergeArtifactProtection(item, artifactItem) {
|
|
1565
|
+
if (item === void 0 || item === null) return item;
|
|
1566
|
+
if (artifactItem === void 0 || artifactItem === null) return item;
|
|
1567
|
+
const a = artifactItem;
|
|
1568
|
+
if (typeof a !== "object") return item;
|
|
1569
|
+
const out = { ...item };
|
|
1570
|
+
if (a._lock !== void 0) out._lock = a._lock;
|
|
1571
|
+
if (a._lockReason !== void 0) out._lockReason = a._lockReason;
|
|
1572
|
+
if (a._packageId !== void 0) out._packageId = a._packageId;
|
|
1573
|
+
if (a._packageVersion !== void 0) out._packageVersion = a._packageVersion;
|
|
1574
|
+
if (a._provenance !== void 0) out._provenance = a._provenance;
|
|
1575
|
+
return out;
|
|
1385
1576
|
}
|
|
1386
1577
|
function simpleHash(str) {
|
|
1387
1578
|
let hash = 0;
|
|
@@ -1514,6 +1705,32 @@ function extractPathValues(item, path) {
|
|
|
1514
1705
|
}
|
|
1515
1706
|
return out;
|
|
1516
1707
|
}
|
|
1708
|
+
function diffShallow(from, to) {
|
|
1709
|
+
const added = [];
|
|
1710
|
+
const removed = [];
|
|
1711
|
+
const changed = [];
|
|
1712
|
+
const fromKeys = new Set(Object.keys(from ?? {}));
|
|
1713
|
+
const toKeys = new Set(Object.keys(to ?? {}));
|
|
1714
|
+
for (const k of toKeys) {
|
|
1715
|
+
if (!fromKeys.has(k)) {
|
|
1716
|
+
added.push({ path: k, value: to[k] });
|
|
1717
|
+
} else {
|
|
1718
|
+
const a = from[k];
|
|
1719
|
+
const b = to[k];
|
|
1720
|
+
const aStr = JSON.stringify(a);
|
|
1721
|
+
const bStr = JSON.stringify(b);
|
|
1722
|
+
if (aStr !== bStr) {
|
|
1723
|
+
changed.push({ path: k, from: a, to: b });
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
for (const k of fromKeys) {
|
|
1728
|
+
if (!toKeys.has(k)) {
|
|
1729
|
+
removed.push({ path: k, value: from[k] });
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
return { added, removed, changed };
|
|
1733
|
+
}
|
|
1517
1734
|
function detectDestructiveObjectChanges(prev, next) {
|
|
1518
1735
|
if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
|
|
1519
1736
|
const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
|
|
@@ -1645,6 +1862,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1645
1862
|
}
|
|
1646
1863
|
}
|
|
1647
1864
|
}
|
|
1865
|
+
const draftPartialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id) WHERE state = 'draft'";
|
|
1866
|
+
try {
|
|
1867
|
+
await exec(draftPartialSql);
|
|
1868
|
+
} catch (err) {
|
|
1869
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1870
|
+
if (/partial|where clause|syntax/i.test(msg)) {
|
|
1871
|
+
try {
|
|
1872
|
+
await exec(
|
|
1873
|
+
"CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id)"
|
|
1874
|
+
);
|
|
1875
|
+
} catch {
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1648
1879
|
} catch {
|
|
1649
1880
|
}
|
|
1650
1881
|
}
|
|
@@ -1768,8 +1999,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1768
1999
|
DEFAULT_METADATA_TYPE_REGISTRY2.map((e) => [e.type, e])
|
|
1769
2000
|
);
|
|
1770
2001
|
const entries = allTypes.map((type) => {
|
|
1771
|
-
const singular =
|
|
1772
|
-
const zodSchema = singular
|
|
2002
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
2003
|
+
const zodSchema = getMetadataTypeSchema2(singular);
|
|
1773
2004
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
1774
2005
|
const form = TYPE_TO_FORM[singular];
|
|
1775
2006
|
const base = registryByType.get(singular);
|
|
@@ -1810,19 +2041,82 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1810
2041
|
});
|
|
1811
2042
|
return { types: allTypes, entries };
|
|
1812
2043
|
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Sweep all (or filtered) metadata types and report entries that
|
|
2046
|
+
* fail spec validation. Powers the Studio governance view
|
|
2047
|
+
* (`GET /api/v1/meta/diagnostics`) and `os doctor`-style CLI
|
|
2048
|
+
* checks.
|
|
2049
|
+
*
|
|
2050
|
+
* `severity` defaults to `'error'` — only entries with at least
|
|
2051
|
+
* one Zod error issue are returned. `'warning'` includes
|
|
2052
|
+
* everything we surface (warnings are reserved for a future lint
|
|
2053
|
+
* layer on top of spec validation).
|
|
2054
|
+
*
|
|
2055
|
+
* `type` may be either a singular (`'view'`) or plural (`'views'`)
|
|
2056
|
+
* identifier; the underlying `getMetaItems` already normalises.
|
|
2057
|
+
*
|
|
2058
|
+
* Implementation note: leverages the `_diagnostics` already
|
|
2059
|
+
* decorated onto items by `getMetaItems()` to avoid running
|
|
2060
|
+
* `safeParse()` twice. For types whose schema is unregistered we
|
|
2061
|
+
* skip silently (they cannot be validated and should not appear
|
|
2062
|
+
* as "valid" either — they are simply opaque to this report).
|
|
2063
|
+
*/
|
|
2064
|
+
async getMetaDiagnostics(request = {}) {
|
|
2065
|
+
const includeWarnings = request.severity === "warning";
|
|
2066
|
+
const targetTypes = request.type ? [request.type] : DEFAULT_METADATA_TYPE_REGISTRY2.filter((e) => getMetadataTypeSchema2(e.type)).map((e) => e.type);
|
|
2067
|
+
const entries = [];
|
|
2068
|
+
const stats = {};
|
|
2069
|
+
let scannedItems = 0;
|
|
2070
|
+
for (const t of targetTypes) {
|
|
2071
|
+
let listed;
|
|
2072
|
+
try {
|
|
2073
|
+
listed = await this.getMetaItems({
|
|
2074
|
+
type: t,
|
|
2075
|
+
organizationId: request.organizationId,
|
|
2076
|
+
packageId: request.packageId
|
|
2077
|
+
});
|
|
2078
|
+
} catch {
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
|
|
2082
|
+
const pkgSet = /* @__PURE__ */ new Set();
|
|
2083
|
+
for (const item of items) {
|
|
2084
|
+
scannedItems += 1;
|
|
2085
|
+
const pkg = item?._packageId ?? null;
|
|
2086
|
+
if (pkg) pkgSet.add(pkg);
|
|
2087
|
+
const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
|
|
2088
|
+
if (!diag) continue;
|
|
2089
|
+
if (diag.valid && !includeWarnings) continue;
|
|
2090
|
+
if (diag.valid && includeWarnings && !diag.warnings?.length) continue;
|
|
2091
|
+
entries.push({
|
|
2092
|
+
type: t,
|
|
2093
|
+
name: typeof item?.name === "string" ? item.name : "<unknown>",
|
|
2094
|
+
diagnostics: diag
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
stats[t] = { count: items.length, packages: [...pkgSet].sort() };
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
entries,
|
|
2101
|
+
total: entries.length,
|
|
2102
|
+
scannedTypes: targetTypes.length,
|
|
2103
|
+
scannedItems,
|
|
2104
|
+
stats
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
1813
2107
|
async getMetaItems(request) {
|
|
1814
2108
|
const { packageId } = request;
|
|
1815
2109
|
let items = [];
|
|
1816
2110
|
if (this.environmentId === void 0) {
|
|
1817
2111
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1818
2112
|
if (items.length === 0) {
|
|
1819
|
-
const alt =
|
|
2113
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1820
2114
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1821
2115
|
}
|
|
1822
2116
|
} else {
|
|
1823
2117
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1824
2118
|
if (items.length === 0) {
|
|
1825
|
-
const alt =
|
|
2119
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1826
2120
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1827
2121
|
}
|
|
1828
2122
|
}
|
|
@@ -1837,7 +2131,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1837
2131
|
if (packageId) whereClause._packageId = packageId;
|
|
1838
2132
|
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
1839
2133
|
if (!rs || rs.length === 0) {
|
|
1840
|
-
const alt =
|
|
2134
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1841
2135
|
if (alt) {
|
|
1842
2136
|
const altWhere = { type: alt, state: "active", organization_id: oid };
|
|
1843
2137
|
if (packageId) altWhere._packageId = packageId;
|
|
@@ -1904,28 +2198,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1904
2198
|
}
|
|
1905
2199
|
return {
|
|
1906
2200
|
type: request.type,
|
|
1907
|
-
items
|
|
2201
|
+
items: decorateMetadataItems(
|
|
2202
|
+
request.type,
|
|
2203
|
+
items.map((it) => {
|
|
2204
|
+
const a = this.lookupArtifactItem(
|
|
2205
|
+
request.type,
|
|
2206
|
+
it?.name
|
|
2207
|
+
);
|
|
2208
|
+
return mergeArtifactProtection(it, a);
|
|
2209
|
+
})
|
|
2210
|
+
)
|
|
1908
2211
|
};
|
|
1909
2212
|
}
|
|
1910
2213
|
async getMetaItem(request) {
|
|
1911
2214
|
let item;
|
|
1912
2215
|
const orgId = request.organizationId;
|
|
2216
|
+
const readState = request.state === "draft" ? "draft" : "active";
|
|
1913
2217
|
try {
|
|
1914
2218
|
const findOverlay = async (oid) => {
|
|
1915
2219
|
const where = {
|
|
1916
2220
|
type: request.type,
|
|
1917
2221
|
name: request.name,
|
|
1918
|
-
state:
|
|
2222
|
+
state: readState,
|
|
1919
2223
|
organization_id: oid
|
|
1920
2224
|
};
|
|
1921
2225
|
const rec = await this.engine.findOne("sys_metadata", { where });
|
|
1922
2226
|
if (rec) return rec;
|
|
1923
|
-
const alt =
|
|
2227
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1924
2228
|
if (alt) {
|
|
1925
2229
|
const altWhere = {
|
|
1926
2230
|
type: alt,
|
|
1927
2231
|
name: request.name,
|
|
1928
|
-
state:
|
|
2232
|
+
state: readState,
|
|
1929
2233
|
organization_id: oid
|
|
1930
2234
|
};
|
|
1931
2235
|
return await this.engine.findOne("sys_metadata", { where: altWhere });
|
|
@@ -1938,6 +2242,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1938
2242
|
}
|
|
1939
2243
|
} catch {
|
|
1940
2244
|
}
|
|
2245
|
+
if (readState === "draft") {
|
|
2246
|
+
if (item === void 0) {
|
|
2247
|
+
const err = new Error(
|
|
2248
|
+
`[no_draft] No pending draft exists for ${request.type}/${request.name}.`
|
|
2249
|
+
);
|
|
2250
|
+
err.code = "no_draft";
|
|
2251
|
+
err.status = 404;
|
|
2252
|
+
throw err;
|
|
2253
|
+
}
|
|
2254
|
+
return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, item) };
|
|
2255
|
+
}
|
|
1941
2256
|
if (item === void 0) {
|
|
1942
2257
|
try {
|
|
1943
2258
|
const services = this.getServicesRegistry?.();
|
|
@@ -1947,7 +2262,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1947
2262
|
if (fromService !== void 0 && fromService !== null) {
|
|
1948
2263
|
item = fromService;
|
|
1949
2264
|
} else {
|
|
1950
|
-
const alt =
|
|
2265
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1951
2266
|
if (alt) {
|
|
1952
2267
|
const altFromService = await metadataService.get(alt, request.name);
|
|
1953
2268
|
if (altFromService !== void 0 && altFromService !== null) {
|
|
@@ -1962,14 +2277,30 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1962
2277
|
if (item === void 0) {
|
|
1963
2278
|
item = this.engine.registry.getItem(request.type, request.name);
|
|
1964
2279
|
if (item === void 0) {
|
|
1965
|
-
const alt =
|
|
2280
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1966
2281
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
1967
2282
|
}
|
|
1968
2283
|
}
|
|
2284
|
+
const artifactItem = this.lookupArtifactItem(request.type, request.name);
|
|
2285
|
+
const decorated = decorateMetadataItem(
|
|
2286
|
+
request.type,
|
|
2287
|
+
mergeArtifactProtection(item, artifactItem)
|
|
2288
|
+
);
|
|
2289
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
2290
|
+
const lockState = resolveLockState(decorated, artifactBacked);
|
|
1969
2291
|
return {
|
|
1970
2292
|
type: request.type,
|
|
1971
2293
|
name: request.name,
|
|
1972
|
-
item
|
|
2294
|
+
item: decorated,
|
|
2295
|
+
lock: lockState.lock,
|
|
2296
|
+
...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
|
|
2297
|
+
...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
|
|
2298
|
+
...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
|
|
2299
|
+
...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
|
|
2300
|
+
...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
|
|
2301
|
+
editable: lockState.editable,
|
|
2302
|
+
deletable: lockState.deletable,
|
|
2303
|
+
resettable: lockState.resettable
|
|
1973
2304
|
};
|
|
1974
2305
|
}
|
|
1975
2306
|
/**
|
|
@@ -1995,7 +2326,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1995
2326
|
if (metadataService && typeof metadataService.get === "function") {
|
|
1996
2327
|
let fromService = await metadataService.get(request.type, request.name);
|
|
1997
2328
|
if (fromService === void 0 || fromService === null) {
|
|
1998
|
-
const alt =
|
|
2329
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1999
2330
|
if (alt) fromService = await metadataService.get(alt, request.name);
|
|
2000
2331
|
}
|
|
2001
2332
|
if (fromService !== void 0 && fromService !== null) code = fromService;
|
|
@@ -2005,7 +2336,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2005
2336
|
if (code === null) {
|
|
2006
2337
|
let regItem = this.engine.registry.getItem(request.type, request.name);
|
|
2007
2338
|
if (regItem === void 0) {
|
|
2008
|
-
const alt =
|
|
2339
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2009
2340
|
if (alt) regItem = this.engine.registry.getItem(alt, request.name);
|
|
2010
2341
|
}
|
|
2011
2342
|
if (regItem !== void 0) code = regItem;
|
|
@@ -2022,7 +2353,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2022
2353
|
};
|
|
2023
2354
|
let rec = await this.engine.findOne("sys_metadata", { where });
|
|
2024
2355
|
if (!rec) {
|
|
2025
|
-
const alt =
|
|
2356
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2026
2357
|
if (alt) {
|
|
2027
2358
|
rec = await this.engine.findOne("sys_metadata", {
|
|
2028
2359
|
where: { ...where, type: alt }
|
|
@@ -2048,15 +2379,80 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2048
2379
|
} catch {
|
|
2049
2380
|
}
|
|
2050
2381
|
const effective = overlay ?? code;
|
|
2382
|
+
const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
|
|
2383
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
2384
|
+
const lockSource = code ?? overlay ?? {};
|
|
2385
|
+
const lockState = resolveLockState(lockSource, artifactBacked);
|
|
2051
2386
|
return {
|
|
2052
2387
|
type: request.type,
|
|
2053
2388
|
name: request.name,
|
|
2054
2389
|
code,
|
|
2055
2390
|
overlay,
|
|
2056
2391
|
overlayScope,
|
|
2057
|
-
effective
|
|
2392
|
+
effective,
|
|
2393
|
+
..._diagnostics ? { _diagnostics } : {},
|
|
2394
|
+
lock: lockState.lock,
|
|
2395
|
+
...lockState.lockReason !== void 0 ? { lockReason: lockState.lockReason } : {},
|
|
2396
|
+
...lockState.lockSource !== void 0 ? { lockSource: lockState.lockSource } : {},
|
|
2397
|
+
...lockState.provenance !== void 0 ? { provenance: lockState.provenance } : {},
|
|
2398
|
+
...lockState.packageId !== void 0 ? { packageId: lockState.packageId } : {},
|
|
2399
|
+
...lockState.packageVersion !== void 0 ? { packageVersion: lockState.packageVersion } : {},
|
|
2400
|
+
editable: lockState.editable,
|
|
2401
|
+
deletable: lockState.deletable,
|
|
2402
|
+
resettable: lockState.resettable
|
|
2058
2403
|
};
|
|
2059
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* ADR-0010 §3.6 / Phase 4.1 — read the metadata-protection audit log
|
|
2407
|
+
* for a single item. Returns the most-recent rows of
|
|
2408
|
+
* `sys_metadata_audit` for this (type, name) tuple, sorted newest
|
|
2409
|
+
* first. Refused (`denied`) and forced (`forced`) writes both appear
|
|
2410
|
+
* here — they never reach the `history` endpoint, which only tracks
|
|
2411
|
+
* successful body snapshots.
|
|
2412
|
+
*
|
|
2413
|
+
* The table is provisioned by `platform-objects` and is the
|
|
2414
|
+
* compliance surface for the lock-enforcement story. When the
|
|
2415
|
+
* environment has not yet provisioned the table (legacy install
|
|
2416
|
+
* prior to ADR-0010) the call returns `{ events: [] }` instead of
|
|
2417
|
+
* raising, keeping the Studio tab harmless.
|
|
2418
|
+
*/
|
|
2419
|
+
async auditMetaItem(request) {
|
|
2420
|
+
const singular = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
2421
|
+
const limit = Math.min(
|
|
2422
|
+
Math.max(1, request.limit ?? 100),
|
|
2423
|
+
500
|
|
2424
|
+
);
|
|
2425
|
+
try {
|
|
2426
|
+
const where = {
|
|
2427
|
+
type: singular,
|
|
2428
|
+
name: request.name
|
|
2429
|
+
};
|
|
2430
|
+
const rows = await this.engine.find("sys_metadata_audit", {
|
|
2431
|
+
where,
|
|
2432
|
+
orderBy: [{ field: "occurred_at", direction: "desc" }],
|
|
2433
|
+
limit
|
|
2434
|
+
});
|
|
2435
|
+
const events = (Array.isArray(rows) ? rows : []).map((r) => ({
|
|
2436
|
+
id: r.id,
|
|
2437
|
+
occurredAt: typeof r.occurred_at === "string" ? r.occurred_at : r.occurred_at instanceof Date ? r.occurred_at.toISOString() : String(r.occurred_at ?? ""),
|
|
2438
|
+
actor: String(r.actor ?? "system"),
|
|
2439
|
+
source: r.source ?? null,
|
|
2440
|
+
operation: r.operation,
|
|
2441
|
+
outcome: r.outcome,
|
|
2442
|
+
code: String(r.code ?? ""),
|
|
2443
|
+
lockState: r.lock_state ?? null,
|
|
2444
|
+
lockOverridden: Boolean(r.lock_overridden),
|
|
2445
|
+
requestId: r.request_id ?? null,
|
|
2446
|
+
note: r.note ?? null
|
|
2447
|
+
}));
|
|
2448
|
+
return { events };
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
console.warn(
|
|
2451
|
+
`[Protocol] auditMetaItem read failed for ${request.type}/${request.name}: ${err?.message ?? err}`
|
|
2452
|
+
);
|
|
2453
|
+
return { events: [] };
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2060
2456
|
async getUiView(request) {
|
|
2061
2457
|
const schema = this.engine.registry.getObject(request.object);
|
|
2062
2458
|
if (!schema) throw new Error(`Object ${request.object} not found`);
|
|
@@ -2935,7 +3331,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2935
3331
|
for (const tok of raw.split(",")) {
|
|
2936
3332
|
const t = tok.trim();
|
|
2937
3333
|
if (!t) continue;
|
|
2938
|
-
const singular =
|
|
3334
|
+
const singular = PLURAL_TO_SINGULAR3[t] ?? t;
|
|
2939
3335
|
set.add(singular);
|
|
2940
3336
|
const plural = SINGULAR_TO_PLURAL2[singular];
|
|
2941
3337
|
if (plural) set.add(plural);
|
|
@@ -2949,13 +3345,201 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2949
3345
|
}
|
|
2950
3346
|
/** Normalize plural→singular before consulting the allow-list. */
|
|
2951
3347
|
static isOverlayAllowed(type) {
|
|
2952
|
-
const singular =
|
|
3348
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
2953
3349
|
if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
|
|
2954
3350
|
return true;
|
|
2955
3351
|
}
|
|
2956
3352
|
const env = this.envWritableTypes();
|
|
2957
3353
|
return env.has(singular) || env.has(type);
|
|
2958
3354
|
}
|
|
3355
|
+
/** Does this type permit creating brand-new (artifact-free) items? */
|
|
3356
|
+
static isRuntimeCreateAllowed(type) {
|
|
3357
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
3358
|
+
if (this.RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || this.RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
3359
|
+
return true;
|
|
3360
|
+
}
|
|
3361
|
+
if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) {
|
|
3362
|
+
return true;
|
|
3363
|
+
}
|
|
3364
|
+
return false;
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Does an artifact (npm-package-loaded) item exist at `(type, name)`?
|
|
3368
|
+
*
|
|
3369
|
+
* The schema registry's `_packageId` tag is set only when
|
|
3370
|
+
* `registerItem(..., packageId)` is called with a truthy packageId
|
|
3371
|
+
* — and only artifact loaders do that. DB-rehydrated items
|
|
3372
|
+
* (sys_metadata rows registered back into the registry by
|
|
3373
|
+
* `getMetaItems` / `loadMetaFromDb`) call `registerItem` without a
|
|
3374
|
+
* packageId, so they carry no `_packageId` and are correctly
|
|
3375
|
+
* excluded here.
|
|
3376
|
+
*
|
|
3377
|
+
* Used by the two-tier authorization model to distinguish
|
|
3378
|
+
* "overlaying a packaged item" (requires `allowOrgOverride`) from
|
|
3379
|
+
* "authoring a DB-only item" (requires only `allowRuntimeCreate`).
|
|
3380
|
+
*/
|
|
3381
|
+
isArtifactBacked(type, name) {
|
|
3382
|
+
const registry = this.engine?.registry;
|
|
3383
|
+
if (!registry || typeof registry.getItem !== "function") {
|
|
3384
|
+
return false;
|
|
3385
|
+
}
|
|
3386
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
3387
|
+
const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3388
|
+
if (!item || !item._packageId) return false;
|
|
3389
|
+
return item._packageId !== "sys_metadata";
|
|
3390
|
+
}
|
|
3391
|
+
// ───────────────────────────────────────────────────────────────────
|
|
3392
|
+
// ADR-0010 — metadata protection (Phase 1: L3 item-level lock)
|
|
3393
|
+
// ───────────────────────────────────────────────────────────────────
|
|
3394
|
+
/**
|
|
3395
|
+
* Look up an item from the artifact registry across both the requested
|
|
3396
|
+
* type and its singular/plural twin. Returns `undefined` when the
|
|
3397
|
+
* registry is unavailable or the item is not artifact-backed.
|
|
3398
|
+
*/
|
|
3399
|
+
lookupArtifactItem(type, name) {
|
|
3400
|
+
const registry = this.engine?.registry;
|
|
3401
|
+
if (!registry || typeof registry.getItem !== "function") return void 0;
|
|
3402
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
3403
|
+
return registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Resolve the effective `_lock` for an item by consulting the
|
|
3407
|
+
* artifact registry first, then the persisted overlay row. Artifact
|
|
3408
|
+
* always wins — by design, an overlay cannot loosen a packaged
|
|
3409
|
+
* lock (ADR-0010 §3.3).
|
|
3410
|
+
*
|
|
3411
|
+
* Returns `'none'` when nothing is locked, which is the common
|
|
3412
|
+
* case. Safe to call when `environmentId` is undefined (control-
|
|
3413
|
+
* plane bootstrap) — the lock check is only meaningful in tenant
|
|
3414
|
+
* scope and the caller is expected to also gate on `environmentId`.
|
|
3415
|
+
*/
|
|
3416
|
+
async getEffectiveLock(type, name, organizationId) {
|
|
3417
|
+
const registry = this.engine?.registry;
|
|
3418
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
3419
|
+
let artifactItem;
|
|
3420
|
+
if (registry && typeof registry.getItem === "function") {
|
|
3421
|
+
artifactItem = registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3422
|
+
}
|
|
3423
|
+
if (artifactItem && artifactItem._packageId && artifactItem._packageId !== "sys_metadata") {
|
|
3424
|
+
const p = extractProtection(artifactItem);
|
|
3425
|
+
if (p.lock !== "none") {
|
|
3426
|
+
return { lock: p.lock, lockReason: p.lockReason, lockSource: "artifact" };
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
try {
|
|
3430
|
+
const where = {
|
|
3431
|
+
type,
|
|
3432
|
+
name,
|
|
3433
|
+
state: "active",
|
|
3434
|
+
organization_id: organizationId ?? null
|
|
3435
|
+
};
|
|
3436
|
+
const row = await this.engine.findOne("sys_metadata", { where });
|
|
3437
|
+
if (row) {
|
|
3438
|
+
const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
3439
|
+
const p = extractProtection(body);
|
|
3440
|
+
if (p.lock !== "none") {
|
|
3441
|
+
return { lock: p.lock, lockReason: p.lockReason, lockSource: "overlay" };
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
} catch {
|
|
3445
|
+
}
|
|
3446
|
+
return { lock: "none", lockReason: void 0, lockSource: void 0 };
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* Best-effort audit-row writer (ADR-0010 §3.6). Failures here are
|
|
3450
|
+
* logged but never block the underlying decision: an environment
|
|
3451
|
+
* without the audit table provisioned (legacy installs before this
|
|
3452
|
+
* ADR landed) still answers normal API calls, just without the
|
|
3453
|
+
* compliance trail. Phase 2 will make the audit table a hard
|
|
3454
|
+
* dependency.
|
|
3455
|
+
*/
|
|
3456
|
+
async recordMetadataAudit(entry) {
|
|
3457
|
+
try {
|
|
3458
|
+
await this.engine.insert("sys_metadata_audit", {
|
|
3459
|
+
occurred_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3460
|
+
actor: entry.actor ?? "system",
|
|
3461
|
+
source: entry.source ?? "protocol",
|
|
3462
|
+
type: PLURAL_TO_SINGULAR3[entry.type] ?? entry.type,
|
|
3463
|
+
name: entry.name,
|
|
3464
|
+
organization_id: entry.organizationId ?? null,
|
|
3465
|
+
operation: entry.operation,
|
|
3466
|
+
outcome: entry.outcome,
|
|
3467
|
+
code: entry.code,
|
|
3468
|
+
lock_state: entry.lockState ?? "none",
|
|
3469
|
+
lock_overridden: entry.lockOverridden ?? false,
|
|
3470
|
+
request_id: entry.requestId ?? null,
|
|
3471
|
+
note: entry.note ?? null
|
|
3472
|
+
});
|
|
3473
|
+
} catch (err) {
|
|
3474
|
+
console.warn(
|
|
3475
|
+
`[Protocol] sys_metadata_audit write failed for ${entry.type}/${entry.name}: ${err?.message ?? err}`
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Phase 1 L3 enforcement for write operations (save / publish /
|
|
3481
|
+
* rollback). Returns null on allow. Returns the structured `Error`
|
|
3482
|
+
* the caller should `throw` on deny — also records the denial in
|
|
3483
|
+
* the audit log so refused attempts are visible in compliance
|
|
3484
|
+
* reports (refused writes never reach sys_metadata_history).
|
|
3485
|
+
*/
|
|
3486
|
+
async assertLockAllowsWrite(args) {
|
|
3487
|
+
if (this.environmentId === void 0) return null;
|
|
3488
|
+
const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
|
|
3489
|
+
const refusal = evaluateLockForWrite(state.lock);
|
|
3490
|
+
if (!refusal) return null;
|
|
3491
|
+
const reason = state.lockReason ?? refusal.reason;
|
|
3492
|
+
const err = new Error(
|
|
3493
|
+
`[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
|
|
3494
|
+
);
|
|
3495
|
+
err.code = "item_locked";
|
|
3496
|
+
err.status = 403;
|
|
3497
|
+
err.lock = state.lock;
|
|
3498
|
+
err.lockReason = reason;
|
|
3499
|
+
await this.recordMetadataAudit({
|
|
3500
|
+
type: args.type,
|
|
3501
|
+
name: args.name,
|
|
3502
|
+
organizationId: args.organizationId ?? null,
|
|
3503
|
+
operation: args.operation,
|
|
3504
|
+
outcome: "denied",
|
|
3505
|
+
code: "item_locked",
|
|
3506
|
+
lockState: state.lock,
|
|
3507
|
+
actor: args.actor,
|
|
3508
|
+
source: args.source ?? `protocol.${args.operation}MetaItem`,
|
|
3509
|
+
requestId: args.requestId,
|
|
3510
|
+
note: reason
|
|
3511
|
+
});
|
|
3512
|
+
return err;
|
|
3513
|
+
}
|
|
3514
|
+
/** Counterpart of {@link assertLockAllowsWrite} for delete. */
|
|
3515
|
+
async assertLockAllowsDelete(args) {
|
|
3516
|
+
if (this.environmentId === void 0) return null;
|
|
3517
|
+
const state = await this.getEffectiveLock(args.type, args.name, args.organizationId ?? null);
|
|
3518
|
+
const refusal = evaluateLockForDelete(state.lock);
|
|
3519
|
+
if (!refusal) return null;
|
|
3520
|
+
const reason = state.lockReason ?? refusal.reason;
|
|
3521
|
+
const err = new Error(
|
|
3522
|
+
`[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ""}). ${reason} \u2014 See ADR-0010 \xA73.3.`
|
|
3523
|
+
);
|
|
3524
|
+
err.code = "item_locked";
|
|
3525
|
+
err.status = 403;
|
|
3526
|
+
err.lock = state.lock;
|
|
3527
|
+
err.lockReason = reason;
|
|
3528
|
+
await this.recordMetadataAudit({
|
|
3529
|
+
type: args.type,
|
|
3530
|
+
name: args.name,
|
|
3531
|
+
organizationId: args.organizationId ?? null,
|
|
3532
|
+
operation: "delete",
|
|
3533
|
+
outcome: "denied",
|
|
3534
|
+
code: "item_locked",
|
|
3535
|
+
lockState: state.lock,
|
|
3536
|
+
actor: args.actor,
|
|
3537
|
+
source: args.source ?? "protocol.deleteMetaItem",
|
|
3538
|
+
requestId: args.requestId,
|
|
3539
|
+
note: reason
|
|
3540
|
+
});
|
|
3541
|
+
return err;
|
|
3542
|
+
}
|
|
2959
3543
|
/**
|
|
2960
3544
|
* Mirror an object-type overlay write into the in-memory engine
|
|
2961
3545
|
* registry so subsequent CRUD finds the new schema. Idempotent and
|
|
@@ -2981,16 +3565,38 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2981
3565
|
if (!request.item) {
|
|
2982
3566
|
throw new Error("Item data is required");
|
|
2983
3567
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
);
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3568
|
+
const mode = request.mode === "draft" ? "draft" : "publish";
|
|
3569
|
+
if (this.environmentId !== void 0) {
|
|
3570
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
3571
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
3572
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
3573
|
+
if (artifactBacked && !overlayAllowed) {
|
|
3574
|
+
const err = new Error(
|
|
3575
|
+
`[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.`
|
|
3576
|
+
);
|
|
3577
|
+
err.code = "not_overridable";
|
|
3578
|
+
err.status = 403;
|
|
3579
|
+
throw err;
|
|
3580
|
+
}
|
|
3581
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
3582
|
+
const err = new Error(
|
|
3583
|
+
`[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.`
|
|
3584
|
+
);
|
|
3585
|
+
err.code = "not_creatable";
|
|
3586
|
+
err.status = 403;
|
|
3587
|
+
throw err;
|
|
3588
|
+
}
|
|
3589
|
+
const lockErr = await this.assertLockAllowsWrite({
|
|
3590
|
+
type: request.type,
|
|
3591
|
+
name: request.name,
|
|
3592
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
3593
|
+
operation: "save",
|
|
3594
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3595
|
+
source: "protocol.saveMetaItem"
|
|
3596
|
+
});
|
|
3597
|
+
if (lockErr) throw lockErr;
|
|
2992
3598
|
}
|
|
2993
|
-
const singularType =
|
|
3599
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
2994
3600
|
if (!request.force && (singularType === "object" || singularType === "field")) {
|
|
2995
3601
|
try {
|
|
2996
3602
|
const existing = await this.getMetaItem({
|
|
@@ -3038,8 +3644,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3038
3644
|
}
|
|
3039
3645
|
}
|
|
3040
3646
|
await this.ensureOverlayIndex();
|
|
3041
|
-
const singularTypeForRepo =
|
|
3042
|
-
|
|
3647
|
+
const singularTypeForRepo = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3648
|
+
const overlayAllowedForRepo = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
3649
|
+
const runtimeCreateAllowedForRepo = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
3650
|
+
const useRepoPath = overlayAllowedForRepo || runtimeCreateAllowedForRepo;
|
|
3651
|
+
if (useRepoPath) {
|
|
3652
|
+
const artifactBacked = this.isArtifactBacked(singularTypeForRepo, request.name);
|
|
3653
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3043
3654
|
const orgId = request.organizationId ?? null;
|
|
3044
3655
|
const repo = this.getOverlayRepo(orgId);
|
|
3045
3656
|
const ref = {
|
|
@@ -3051,21 +3662,37 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3051
3662
|
if (request.parentVersion !== void 0) {
|
|
3052
3663
|
parentVersion = request.parentVersion;
|
|
3053
3664
|
} else {
|
|
3054
|
-
const current = await repo.get(ref);
|
|
3665
|
+
const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
|
|
3055
3666
|
parentVersion = current?.hash ?? null;
|
|
3056
3667
|
}
|
|
3057
3668
|
try {
|
|
3058
3669
|
const result = await repo.put(ref, request.item, {
|
|
3059
3670
|
parentVersion,
|
|
3060
3671
|
actor: request.actor ?? "system",
|
|
3061
|
-
source: "protocol.saveMetaItem"
|
|
3672
|
+
source: "protocol.saveMetaItem",
|
|
3673
|
+
intent,
|
|
3674
|
+
state: mode === "draft" ? "draft" : "active"
|
|
3675
|
+
});
|
|
3676
|
+
if (mode === "publish") {
|
|
3677
|
+
this.applyObjectRegistryMutation(request);
|
|
3678
|
+
}
|
|
3679
|
+
await this.recordMetadataAudit({
|
|
3680
|
+
type: request.type,
|
|
3681
|
+
name: request.name,
|
|
3682
|
+
organizationId: orgId,
|
|
3683
|
+
operation: "save",
|
|
3684
|
+
outcome: "allowed",
|
|
3685
|
+
code: "ok",
|
|
3686
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3687
|
+
source: "protocol.saveMetaItem",
|
|
3688
|
+
note: mode === "draft" ? "draft" : "active"
|
|
3062
3689
|
});
|
|
3063
|
-
this.applyObjectRegistryMutation(request);
|
|
3064
3690
|
return {
|
|
3065
3691
|
success: true,
|
|
3066
3692
|
version: result.version,
|
|
3067
3693
|
seq: result.seq,
|
|
3068
|
-
|
|
3694
|
+
state: mode === "draft" ? "draft" : "active",
|
|
3695
|
+
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}]`
|
|
3069
3696
|
};
|
|
3070
3697
|
} catch (err) {
|
|
3071
3698
|
if (err instanceof ConflictError2) {
|
|
@@ -3149,8 +3776,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3149
3776
|
* "no history" uniformly.
|
|
3150
3777
|
*/
|
|
3151
3778
|
async historyMetaItem(request) {
|
|
3152
|
-
const singularType =
|
|
3153
|
-
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
|
|
3779
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3780
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3154
3781
|
return { events: [] };
|
|
3155
3782
|
}
|
|
3156
3783
|
const orgId = request.organizationId ?? null;
|
|
@@ -3168,22 +3795,264 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3168
3795
|
return { events };
|
|
3169
3796
|
}
|
|
3170
3797
|
/**
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3173
|
-
*
|
|
3174
|
-
* with {@link saveMetaItem}.
|
|
3798
|
+
* Promote the pending draft overlay to the live (`active`) row.
|
|
3799
|
+
* Records a history event with `op='publish'`. 404 (`[no_draft]`)
|
|
3800
|
+
* when there is nothing to publish.
|
|
3175
3801
|
*/
|
|
3176
|
-
async
|
|
3177
|
-
|
|
3802
|
+
async publishMetaItem(request) {
|
|
3803
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3804
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3805
|
+
const err = new Error(
|
|
3806
|
+
`[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
|
|
3807
|
+
);
|
|
3808
|
+
err.code = "not_overridable";
|
|
3809
|
+
err.status = 403;
|
|
3810
|
+
throw err;
|
|
3811
|
+
}
|
|
3812
|
+
const _publishLockErr = await this.assertLockAllowsWrite({
|
|
3813
|
+
type: request.type,
|
|
3814
|
+
name: request.name,
|
|
3815
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
3816
|
+
operation: "publish",
|
|
3817
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3818
|
+
source: "protocol.publishMetaItem"
|
|
3819
|
+
});
|
|
3820
|
+
if (_publishLockErr) throw _publishLockErr;
|
|
3821
|
+
await this.ensureOverlayIndex();
|
|
3822
|
+
const orgId = request.organizationId ?? null;
|
|
3823
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3824
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3825
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3826
|
+
const ref = {
|
|
3827
|
+
type: singularType,
|
|
3828
|
+
name: request.name,
|
|
3829
|
+
org: orgId ?? "env"
|
|
3830
|
+
};
|
|
3831
|
+
try {
|
|
3832
|
+
const result = await repo.promoteDraft(ref, {
|
|
3833
|
+
actor: request.actor ?? "system",
|
|
3834
|
+
source: "protocol.publishMetaItem",
|
|
3835
|
+
...request.message ? { message: request.message } : {},
|
|
3836
|
+
intent
|
|
3837
|
+
});
|
|
3838
|
+
this.applyObjectRegistryMutation({
|
|
3839
|
+
type: request.type,
|
|
3840
|
+
name: request.name,
|
|
3841
|
+
item: result.item.body
|
|
3842
|
+
});
|
|
3843
|
+
return {
|
|
3844
|
+
success: true,
|
|
3845
|
+
version: result.version,
|
|
3846
|
+
seq: result.seq,
|
|
3847
|
+
message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3848
|
+
};
|
|
3849
|
+
} catch (err) {
|
|
3850
|
+
if (err instanceof ConflictError2) {
|
|
3851
|
+
const conflict = new Error(
|
|
3852
|
+
`[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"}.`
|
|
3853
|
+
);
|
|
3854
|
+
conflict.code = "metadata_conflict";
|
|
3855
|
+
conflict.status = 409;
|
|
3856
|
+
conflict.expectedParent = err.expectedParent;
|
|
3857
|
+
conflict.actualHead = err.actualHead;
|
|
3858
|
+
throw conflict;
|
|
3859
|
+
}
|
|
3860
|
+
throw err;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Restore the body recorded at history `toVersion` as the new
|
|
3865
|
+
* live row. Writes a history event with `op='revert'`. 404
|
|
3866
|
+
* (`[version_not_found]`) when the target version doesn't exist;
|
|
3867
|
+
* 409 (`[version_not_restorable]`) when the target is a delete
|
|
3868
|
+
* tombstone (no body to bring back).
|
|
3869
|
+
*/
|
|
3870
|
+
async rollbackMetaItem(request) {
|
|
3871
|
+
if (!Number.isFinite(request.toVersion) || request.toVersion < 1) {
|
|
3178
3872
|
const err = new Error(
|
|
3179
|
-
`[
|
|
3873
|
+
`[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`
|
|
3874
|
+
);
|
|
3875
|
+
err.code = "invalid_request";
|
|
3876
|
+
err.status = 400;
|
|
3877
|
+
throw err;
|
|
3878
|
+
}
|
|
3879
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3880
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3881
|
+
const err = new Error(
|
|
3882
|
+
`[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
|
|
3180
3883
|
);
|
|
3181
3884
|
err.code = "not_overridable";
|
|
3182
3885
|
err.status = 403;
|
|
3183
3886
|
throw err;
|
|
3184
3887
|
}
|
|
3185
|
-
const
|
|
3186
|
-
|
|
3888
|
+
const _rollbackLockErr = await this.assertLockAllowsWrite({
|
|
3889
|
+
type: request.type,
|
|
3890
|
+
name: request.name,
|
|
3891
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
3892
|
+
operation: "rollback",
|
|
3893
|
+
...request.actor ? { actor: request.actor } : {},
|
|
3894
|
+
source: "protocol.rollbackMetaItem"
|
|
3895
|
+
});
|
|
3896
|
+
if (_rollbackLockErr) throw _rollbackLockErr;
|
|
3897
|
+
await this.ensureOverlayIndex();
|
|
3898
|
+
const orgId = request.organizationId ?? null;
|
|
3899
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3900
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3901
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3902
|
+
const ref = {
|
|
3903
|
+
type: singularType,
|
|
3904
|
+
name: request.name,
|
|
3905
|
+
org: orgId ?? "env"
|
|
3906
|
+
};
|
|
3907
|
+
try {
|
|
3908
|
+
const result = await repo.restoreVersion(ref, request.toVersion, {
|
|
3909
|
+
actor: request.actor ?? "system",
|
|
3910
|
+
source: "protocol.rollbackMetaItem",
|
|
3911
|
+
...request.message ? { message: request.message } : {},
|
|
3912
|
+
intent
|
|
3913
|
+
});
|
|
3914
|
+
this.applyObjectRegistryMutation({
|
|
3915
|
+
type: request.type,
|
|
3916
|
+
name: request.name,
|
|
3917
|
+
item: result.item.body
|
|
3918
|
+
});
|
|
3919
|
+
return {
|
|
3920
|
+
success: true,
|
|
3921
|
+
version: result.version,
|
|
3922
|
+
seq: result.seq,
|
|
3923
|
+
restoredFromVersion: request.toVersion,
|
|
3924
|
+
message: `Reverted to version ${request.toVersion} \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3925
|
+
};
|
|
3926
|
+
} catch (err) {
|
|
3927
|
+
if (err instanceof ConflictError2) {
|
|
3928
|
+
const conflict = new Error(
|
|
3929
|
+
`[metadata_conflict] ${request.type}/${request.name} advanced during rollback. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
|
|
3930
|
+
);
|
|
3931
|
+
conflict.code = "metadata_conflict";
|
|
3932
|
+
conflict.status = 409;
|
|
3933
|
+
conflict.expectedParent = err.expectedParent;
|
|
3934
|
+
conflict.actualHead = err.actualHead;
|
|
3935
|
+
throw conflict;
|
|
3936
|
+
}
|
|
3937
|
+
throw err;
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* Compute a shallow structural diff between two historical
|
|
3942
|
+
* versions of a metadata item. Either side may be omitted: when
|
|
3943
|
+
* `toVersion` is undefined the current active body is used; when
|
|
3944
|
+
* `fromVersion` is undefined the immediately previous history row
|
|
3945
|
+
* is used. Returns `{ added, removed, changed }` keyed by JSON
|
|
3946
|
+
* pointer-style paths for primitive leaves; nested objects/arrays
|
|
3947
|
+
* are reported as a single change record.
|
|
3948
|
+
*/
|
|
3949
|
+
async diffMetaItem(request) {
|
|
3950
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3951
|
+
const orgId = request.organizationId ?? null;
|
|
3952
|
+
const events = (await this.historyMetaItem({
|
|
3953
|
+
type: singularType,
|
|
3954
|
+
name: request.name,
|
|
3955
|
+
...orgId ? { organizationId: orgId } : {}
|
|
3956
|
+
})).events;
|
|
3957
|
+
const versions = events.map((ev) => ev.version).filter((v) => typeof v === "number");
|
|
3958
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3959
|
+
const fullRef = {
|
|
3960
|
+
type: singularType,
|
|
3961
|
+
name: request.name,
|
|
3962
|
+
org: orgId ?? "env"
|
|
3963
|
+
};
|
|
3964
|
+
const histRows = [];
|
|
3965
|
+
try {
|
|
3966
|
+
const engineAny = this.engine;
|
|
3967
|
+
const rows = await engineAny.find("sys_metadata_history", {
|
|
3968
|
+
where: {
|
|
3969
|
+
organization_id: orgId,
|
|
3970
|
+
type: singularType,
|
|
3971
|
+
name: request.name
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
rows.sort((a, b) => (a.version ?? 0) - (b.version ?? 0));
|
|
3975
|
+
for (const r of rows) {
|
|
3976
|
+
const body = r.metadata == null ? null : typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
3977
|
+
histRows.push({ version: r.version ?? 0, body });
|
|
3978
|
+
}
|
|
3979
|
+
} catch {
|
|
3980
|
+
}
|
|
3981
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
3982
|
+
for (const r of histRows) byVersion.set(r.version, r.body);
|
|
3983
|
+
let fromBody = null;
|
|
3984
|
+
let toBody = null;
|
|
3985
|
+
let fromVersion = null;
|
|
3986
|
+
let toVersion = null;
|
|
3987
|
+
if (request.toVersion !== void 0) {
|
|
3988
|
+
toVersion = request.toVersion;
|
|
3989
|
+
toBody = byVersion.get(request.toVersion) ?? null;
|
|
3990
|
+
} else {
|
|
3991
|
+
const current = await repo.get(fullRef, { state: "active" });
|
|
3992
|
+
toBody = current ? current.body : null;
|
|
3993
|
+
toVersion = histRows.length ? histRows[histRows.length - 1].version : null;
|
|
3994
|
+
}
|
|
3995
|
+
if (request.fromVersion !== void 0) {
|
|
3996
|
+
fromVersion = request.fromVersion;
|
|
3997
|
+
fromBody = byVersion.get(request.fromVersion) ?? null;
|
|
3998
|
+
} else if (toVersion !== null) {
|
|
3999
|
+
const sorted = histRows.map((r) => r.version).filter((v) => v < toVersion);
|
|
4000
|
+
if (sorted.length) {
|
|
4001
|
+
fromVersion = sorted[sorted.length - 1];
|
|
4002
|
+
fromBody = byVersion.get(fromVersion) ?? null;
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
const diff = diffShallow(fromBody ?? {}, toBody ?? {});
|
|
4006
|
+
const _used = versions;
|
|
4007
|
+
void _used;
|
|
4008
|
+
return {
|
|
4009
|
+
type: request.type,
|
|
4010
|
+
name: request.name,
|
|
4011
|
+
fromVersion,
|
|
4012
|
+
toVersion,
|
|
4013
|
+
...diff
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Remove a customization overlay row for the given metadata item, so the
|
|
4018
|
+
* next read falls through to the artifact-loaded default. Implements the
|
|
4019
|
+
* "Reset to factory default" semantic from ADR-0005. Whitelist is shared
|
|
4020
|
+
* with {@link saveMetaItem}.
|
|
4021
|
+
*/
|
|
4022
|
+
async deleteMetaItem(request) {
|
|
4023
|
+
if (this.environmentId !== void 0) {
|
|
4024
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
4025
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
4026
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
4027
|
+
if (artifactBacked && !overlayAllowed) {
|
|
4028
|
+
const err = new Error(
|
|
4029
|
+
`[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.`
|
|
4030
|
+
);
|
|
4031
|
+
err.code = "not_overridable";
|
|
4032
|
+
err.status = 403;
|
|
4033
|
+
throw err;
|
|
4034
|
+
}
|
|
4035
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
4036
|
+
const err = new Error(
|
|
4037
|
+
`[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.`
|
|
4038
|
+
);
|
|
4039
|
+
err.code = "not_creatable";
|
|
4040
|
+
err.status = 403;
|
|
4041
|
+
throw err;
|
|
4042
|
+
}
|
|
4043
|
+
const lockErr = await this.assertLockAllowsDelete({
|
|
4044
|
+
type: request.type,
|
|
4045
|
+
name: request.name,
|
|
4046
|
+
...request.organizationId ? { organizationId: request.organizationId } : {},
|
|
4047
|
+
...request.actor ? { actor: request.actor } : {},
|
|
4048
|
+
source: "protocol.deleteMetaItem"
|
|
4049
|
+
});
|
|
4050
|
+
if (lockErr) throw lockErr;
|
|
4051
|
+
}
|
|
4052
|
+
const singularTypeForRepo = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
4053
|
+
const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
4054
|
+
const runtimeCreateAllowedForRepoDel = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
4055
|
+
const useRepoPath = overlayAllowedForRepoDel || runtimeCreateAllowedForRepoDel;
|
|
3187
4056
|
if (useRepoPath) {
|
|
3188
4057
|
const orgId = request.organizationId ?? null;
|
|
3189
4058
|
const repo = this.getOverlayRepo(orgId);
|
|
@@ -3193,19 +4062,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3193
4062
|
org: orgId ?? "env"
|
|
3194
4063
|
};
|
|
3195
4064
|
try {
|
|
3196
|
-
const
|
|
4065
|
+
const targetState = request.state === "draft" ? "draft" : "active";
|
|
4066
|
+
const current = await repo.get(ref, { state: targetState });
|
|
3197
4067
|
if (!current) {
|
|
3198
4068
|
return {
|
|
3199
4069
|
success: true,
|
|
3200
4070
|
reset: false,
|
|
3201
|
-
message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
|
|
4071
|
+
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.`
|
|
3202
4072
|
};
|
|
3203
4073
|
}
|
|
3204
4074
|
const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
|
|
3205
4075
|
const result = await repo.delete(ref, {
|
|
3206
4076
|
parentVersion,
|
|
3207
4077
|
actor: request.actor ?? "system",
|
|
3208
|
-
source: "protocol.deleteMetaItem"
|
|
4078
|
+
source: "protocol.deleteMetaItem",
|
|
4079
|
+
intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
|
|
4080
|
+
state: targetState
|
|
3209
4081
|
});
|
|
3210
4082
|
if (this.environmentId === void 0) {
|
|
3211
4083
|
try {
|
|
@@ -3220,11 +4092,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3220
4092
|
} catch {
|
|
3221
4093
|
}
|
|
3222
4094
|
}
|
|
4095
|
+
await this.recordMetadataAudit({
|
|
4096
|
+
type: request.type,
|
|
4097
|
+
name: request.name,
|
|
4098
|
+
organizationId: orgId,
|
|
4099
|
+
operation: "delete",
|
|
4100
|
+
outcome: "allowed",
|
|
4101
|
+
code: "ok",
|
|
4102
|
+
...request.actor ? { actor: request.actor } : {},
|
|
4103
|
+
source: "protocol.deleteMetaItem",
|
|
4104
|
+
note: targetState
|
|
4105
|
+
});
|
|
3223
4106
|
return {
|
|
3224
4107
|
success: true,
|
|
3225
4108
|
reset: true,
|
|
3226
4109
|
seq: result.seq,
|
|
3227
|
-
message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
|
|
4110
|
+
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}]`
|
|
3228
4111
|
};
|
|
3229
4112
|
} catch (err) {
|
|
3230
4113
|
if (err instanceof ConflictError2) {
|
|
@@ -3302,7 +4185,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3302
4185
|
for (const record of records) {
|
|
3303
4186
|
try {
|
|
3304
4187
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
3305
|
-
const normalizedType =
|
|
4188
|
+
const normalizedType = PLURAL_TO_SINGULAR3[record.type] ?? record.type;
|
|
3306
4189
|
if (normalizedType === "object") {
|
|
3307
4190
|
this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
|
|
3308
4191
|
} else {
|
|
@@ -3336,7 +4219,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3336
4219
|
* — the engine never throws.
|
|
3337
4220
|
*/
|
|
3338
4221
|
async findReferencesToMeta(request) {
|
|
3339
|
-
const singularTarget =
|
|
4222
|
+
const singularTarget = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3340
4223
|
const targetName = request.name;
|
|
3341
4224
|
const matchers = REFERENCE_PATHS[singularTarget];
|
|
3342
4225
|
if (!matchers || matchers.length === 0) {
|
|
@@ -3547,6 +4430,41 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
|
3547
4430
|
* {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
|
|
3548
4431
|
*/
|
|
3549
4432
|
_ObjectStackProtocolImplementation._envWritableTypes = null;
|
|
4433
|
+
/**
|
|
4434
|
+
* Types that opt into runtime creation of brand-new items (ADR-0005
|
|
4435
|
+
* extension — two-tier model). A type may have
|
|
4436
|
+
* `allowOrgOverride: false` (cannot overlay artifact-shipped items)
|
|
4437
|
+
* yet still set `allowRuntimeCreate: true` (users can author new
|
|
4438
|
+
* items in `sys_metadata`). The two flags are orthogonal; see
|
|
4439
|
+
* {@link isArtifactBacked} for how the protocol decides which gate
|
|
4440
|
+
* applies to a given save/delete.
|
|
4441
|
+
*/
|
|
4442
|
+
/**
|
|
4443
|
+
* Set of type names that have a static entry in
|
|
4444
|
+
* `DEFAULT_METADATA_TYPE_REGISTRY`. Anything outside this set is
|
|
4445
|
+
* runtime-registered (plugin-provided types like `theme`, `api`,
|
|
4446
|
+
* `connector`) — the listing endpoint at `getMetaTypes()` synthesises
|
|
4447
|
+
* those with `allowRuntimeCreate: true`, so this gate must agree.
|
|
4448
|
+
*/
|
|
4449
|
+
_ObjectStackProtocolImplementation.STATIC_REGISTRY_TYPES = (() => {
|
|
4450
|
+
const out = /* @__PURE__ */ new Set();
|
|
4451
|
+
for (const entry of DEFAULT_METADATA_TYPE_REGISTRY2) {
|
|
4452
|
+
out.add(entry.type);
|
|
4453
|
+
const plural = SINGULAR_TO_PLURAL2[entry.type];
|
|
4454
|
+
if (plural) out.add(plural);
|
|
4455
|
+
}
|
|
4456
|
+
return out;
|
|
4457
|
+
})();
|
|
4458
|
+
_ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
|
|
4459
|
+
const out = /* @__PURE__ */ new Set();
|
|
4460
|
+
for (const entry of DEFAULT_METADATA_TYPE_REGISTRY2) {
|
|
4461
|
+
if (!entry.allowRuntimeCreate) continue;
|
|
4462
|
+
out.add(entry.type);
|
|
4463
|
+
const plural = SINGULAR_TO_PLURAL2[entry.type];
|
|
4464
|
+
if (plural) out.add(plural);
|
|
4465
|
+
}
|
|
4466
|
+
return out;
|
|
4467
|
+
})();
|
|
3550
4468
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
3551
4469
|
|
|
3552
4470
|
// src/engine.ts
|