@objectstack/objectql 7.0.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 +258 -7
- package/dist/index.d.ts +258 -7
- package/dist/index.js +757 -163
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +749 -155
- 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
|
}
|
|
@@ -626,6 +634,12 @@ import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from "@objectstack/spec/shared
|
|
|
626
634
|
var OVERLAY_ALLOWED_TYPES = new Set(
|
|
627
635
|
DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
|
|
628
636
|
);
|
|
637
|
+
var STATIC_REGISTRY_TYPES = new Set(
|
|
638
|
+
DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type)
|
|
639
|
+
);
|
|
640
|
+
var RUNTIME_CREATE_ALLOWED_TYPES = new Set(
|
|
641
|
+
DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type)
|
|
642
|
+
);
|
|
629
643
|
var _envWritableMetadataTypes = null;
|
|
630
644
|
function envWritableMetadataTypes() {
|
|
631
645
|
if (_envWritableMetadataTypes !== null) return _envWritableMetadataTypes;
|
|
@@ -675,11 +689,16 @@ var SysMetadataRepository = class {
|
|
|
675
689
|
/**
|
|
676
690
|
* Read the current overlay row. Returns null if no row exists —
|
|
677
691
|
* callers (e.g. LayeredRepository) fall through to lower layers.
|
|
692
|
+
*
|
|
693
|
+
* `opts.state` selects which lifecycle row to read: defaults to the
|
|
694
|
+
* live published row (`'active'`). Pass `'draft'` to read the pending
|
|
695
|
+
* unpublished revision (if any).
|
|
678
696
|
*/
|
|
679
|
-
async get(ref) {
|
|
697
|
+
async get(ref, opts) {
|
|
680
698
|
this.assertOpen();
|
|
699
|
+
const state = opts?.state ?? "active";
|
|
681
700
|
const row = await this.engine.findOne("sys_metadata", {
|
|
682
|
-
where: this.whereFor(ref)
|
|
701
|
+
where: this.whereFor(ref, state)
|
|
683
702
|
});
|
|
684
703
|
if (!row) return null;
|
|
685
704
|
return this.rowToItem(ref, row);
|
|
@@ -722,12 +741,13 @@ var SysMetadataRepository = class {
|
|
|
722
741
|
}
|
|
723
742
|
async put(ref, spec, opts) {
|
|
724
743
|
this.assertOpen();
|
|
725
|
-
this.assertAllowed(ref.type);
|
|
744
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
745
|
+
const state = opts.state ?? "active";
|
|
726
746
|
const body = spec ?? {};
|
|
727
747
|
const hash = hashSpec(body);
|
|
728
748
|
const result = await this.withTxn(async (ctx) => {
|
|
729
749
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
730
|
-
where: this.whereFor(ref),
|
|
750
|
+
where: this.whereFor(ref, state),
|
|
731
751
|
context: ctx
|
|
732
752
|
});
|
|
733
753
|
const existingHash = existing?.checksum ?? null;
|
|
@@ -739,7 +759,8 @@ var SysMetadataRepository = class {
|
|
|
739
759
|
return { skipped: true, version: hash, seq: item2.seq, item: item2 };
|
|
740
760
|
}
|
|
741
761
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
742
|
-
const
|
|
762
|
+
const baseOp = existing ? "update" : "create";
|
|
763
|
+
const op = opts.opType ?? baseOp;
|
|
743
764
|
const version = await this.nextItemVersion(ref, ctx);
|
|
744
765
|
const eventSeq = await this.nextEventSeq(ctx);
|
|
745
766
|
const parentRowData = {
|
|
@@ -748,7 +769,7 @@ var SysMetadataRepository = class {
|
|
|
748
769
|
organization_id: this.organizationId,
|
|
749
770
|
metadata: JSON.stringify(body),
|
|
750
771
|
checksum: hash,
|
|
751
|
-
state
|
|
772
|
+
state,
|
|
752
773
|
version,
|
|
753
774
|
updated_at: now
|
|
754
775
|
};
|
|
@@ -814,25 +835,28 @@ var SysMetadataRepository = class {
|
|
|
814
835
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
815
836
|
}
|
|
816
837
|
this.seqCounter = result.seq;
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
838
|
+
if (state === "active") {
|
|
839
|
+
this.broadcast({
|
|
840
|
+
seq: result.seq,
|
|
841
|
+
op: result.op,
|
|
842
|
+
ref: this.fullRef(ref),
|
|
843
|
+
hash: result.version,
|
|
844
|
+
parentHash: result.existingHash,
|
|
845
|
+
actor: result.actor,
|
|
846
|
+
message: result.message,
|
|
847
|
+
ts: result.now,
|
|
848
|
+
source: result.source
|
|
849
|
+
});
|
|
850
|
+
}
|
|
828
851
|
return { version: result.version, seq: result.seq, item: result.item };
|
|
829
852
|
}
|
|
830
853
|
async delete(ref, opts) {
|
|
831
854
|
this.assertOpen();
|
|
832
|
-
this.assertAllowed(ref.type);
|
|
855
|
+
this.assertAllowed(ref.type, opts.intent);
|
|
856
|
+
const state = opts.state ?? "active";
|
|
833
857
|
const result = await this.withTxn(async (ctx) => {
|
|
834
858
|
const existing = await this.engine.findOne("sys_metadata", {
|
|
835
|
-
where: this.whereFor(ref),
|
|
859
|
+
where: this.whereFor(ref, state),
|
|
836
860
|
context: ctx
|
|
837
861
|
});
|
|
838
862
|
if (!existing) {
|
|
@@ -849,32 +873,38 @@ var SysMetadataRepository = class {
|
|
|
849
873
|
);
|
|
850
874
|
}
|
|
851
875
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
852
|
-
|
|
853
|
-
|
|
876
|
+
let version = 0;
|
|
877
|
+
let eventSeq = 0;
|
|
878
|
+
if (state === "active") {
|
|
879
|
+
version = await this.nextItemVersion(ref, ctx);
|
|
880
|
+
eventSeq = await this.nextEventSeq(ctx);
|
|
881
|
+
}
|
|
854
882
|
await this.engine.delete("sys_metadata", {
|
|
855
883
|
where: { id: existingId },
|
|
856
884
|
context: ctx
|
|
857
885
|
});
|
|
858
|
-
|
|
859
|
-
this.
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
886
|
+
if (state === "active") {
|
|
887
|
+
await this.engine.insert(
|
|
888
|
+
this.historyTable,
|
|
889
|
+
{
|
|
890
|
+
id: this.uuid(),
|
|
891
|
+
event_seq: eventSeq,
|
|
892
|
+
type: ref.type,
|
|
893
|
+
name: ref.name,
|
|
894
|
+
version,
|
|
895
|
+
operation_type: "delete",
|
|
896
|
+
metadata: null,
|
|
897
|
+
checksum: null,
|
|
898
|
+
previous_checksum: existingHash,
|
|
899
|
+
change_note: opts.message,
|
|
900
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
901
|
+
organization_id: this.organizationId,
|
|
902
|
+
recorded_by: opts.actor,
|
|
903
|
+
recorded_at: now
|
|
904
|
+
},
|
|
905
|
+
{ context: ctx }
|
|
906
|
+
);
|
|
907
|
+
}
|
|
878
908
|
return {
|
|
879
909
|
eventSeq,
|
|
880
910
|
existingHash,
|
|
@@ -884,20 +914,117 @@ var SysMetadataRepository = class {
|
|
|
884
914
|
actor: opts.actor
|
|
885
915
|
};
|
|
886
916
|
});
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
917
|
+
if (state === "active") {
|
|
918
|
+
this.seqCounter = result.eventSeq;
|
|
919
|
+
this.broadcast({
|
|
920
|
+
seq: result.eventSeq,
|
|
921
|
+
op: "delete",
|
|
922
|
+
ref: this.fullRef(ref),
|
|
923
|
+
hash: null,
|
|
924
|
+
parentHash: result.existingHash,
|
|
925
|
+
actor: result.actor,
|
|
926
|
+
message: result.message,
|
|
927
|
+
ts: result.now,
|
|
928
|
+
source: result.source
|
|
929
|
+
});
|
|
930
|
+
}
|
|
899
931
|
return { seq: result.eventSeq };
|
|
900
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Promote the pending draft row for `ref` into the live (`active`)
|
|
935
|
+
* overlay. Atomic: reads the draft inside the same transaction, runs
|
|
936
|
+
* the canonical `put` to upsert the active row (which appends a
|
|
937
|
+
* history event with `operation_type='publish'`), then deletes the
|
|
938
|
+
* draft row.
|
|
939
|
+
*
|
|
940
|
+
* Errors if no draft exists (callers should 404). The active row's
|
|
941
|
+
* `parentVersion` is computed from the current active hash so this
|
|
942
|
+
* also surfaces optimistic-lock conflicts when something else has
|
|
943
|
+
* published in between (e.g. another admin reverted to an older
|
|
944
|
+
* version since the draft was authored).
|
|
945
|
+
*/
|
|
946
|
+
async promoteDraft(ref, opts) {
|
|
947
|
+
this.assertOpen();
|
|
948
|
+
const draft = await this.get(ref, { state: "draft" });
|
|
949
|
+
if (!draft) {
|
|
950
|
+
const err = new Error(
|
|
951
|
+
`[no_draft] No pending draft exists for ${ref.type}/${ref.name} \u2014 nothing to publish.`
|
|
952
|
+
);
|
|
953
|
+
err.code = "no_draft";
|
|
954
|
+
err.status = 404;
|
|
955
|
+
throw err;
|
|
956
|
+
}
|
|
957
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
958
|
+
const result = await this.put(ref, draft.body, {
|
|
959
|
+
parentVersion: currentActive?.hash ?? null,
|
|
960
|
+
actor: opts.actor,
|
|
961
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
962
|
+
message: opts.message ?? `publish draft (hash ${draft.hash})`,
|
|
963
|
+
intent: opts.intent ?? "override-artifact",
|
|
964
|
+
state: "active",
|
|
965
|
+
opType: "publish"
|
|
966
|
+
});
|
|
967
|
+
try {
|
|
968
|
+
await this.delete(ref, {
|
|
969
|
+
parentVersion: draft.hash,
|
|
970
|
+
actor: opts.actor,
|
|
971
|
+
source: opts.source ?? "sys-metadata-repo.publish",
|
|
972
|
+
intent: opts.intent ?? "override-artifact",
|
|
973
|
+
state: "draft"
|
|
974
|
+
});
|
|
975
|
+
} catch {
|
|
976
|
+
}
|
|
977
|
+
return result;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Restore the body recorded in history at `targetVersion` (per-org
|
|
981
|
+
* lineage counter) as the new active row. Writes a history event
|
|
982
|
+
* with `operation_type='revert'` so the audit trail captures the
|
|
983
|
+
* intent. Does NOT touch any draft row.
|
|
984
|
+
*
|
|
985
|
+
* Throws `[version_not_found]` (404) if the target version row is
|
|
986
|
+
* missing or is a delete tombstone (no body to restore).
|
|
987
|
+
*/
|
|
988
|
+
async restoreVersion(ref, targetVersion, opts) {
|
|
989
|
+
this.assertOpen();
|
|
990
|
+
const full = this.fullRef(ref);
|
|
991
|
+
const row = await this.engine.findOne(this.historyTable, {
|
|
992
|
+
where: {
|
|
993
|
+
organization_id: this.organizationId,
|
|
994
|
+
type: full.type,
|
|
995
|
+
name: full.name,
|
|
996
|
+
version: targetVersion
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
if (!row) {
|
|
1000
|
+
const err = new Error(
|
|
1001
|
+
`[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`
|
|
1002
|
+
);
|
|
1003
|
+
err.code = "version_not_found";
|
|
1004
|
+
err.status = 404;
|
|
1005
|
+
throw err;
|
|
1006
|
+
}
|
|
1007
|
+
const raw = row.metadata;
|
|
1008
|
+
if (raw === null || raw === void 0) {
|
|
1009
|
+
const err = new Error(
|
|
1010
|
+
`[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone \u2014 nothing to restore.`
|
|
1011
|
+
);
|
|
1012
|
+
err.code = "version_not_restorable";
|
|
1013
|
+
err.status = 409;
|
|
1014
|
+
throw err;
|
|
1015
|
+
}
|
|
1016
|
+
const body = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1017
|
+
const currentActive = await this.get(ref, { state: "active" });
|
|
1018
|
+
return this.put(ref, body, {
|
|
1019
|
+
parentVersion: currentActive?.hash ?? null,
|
|
1020
|
+
actor: opts.actor,
|
|
1021
|
+
source: opts.source ?? "sys-metadata-repo.revert",
|
|
1022
|
+
message: opts.message ?? `revert to version ${targetVersion}`,
|
|
1023
|
+
intent: opts.intent ?? "override-artifact",
|
|
1024
|
+
state: "active",
|
|
1025
|
+
opType: "revert"
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
901
1028
|
async *list(filter) {
|
|
902
1029
|
this.assertOpen();
|
|
903
1030
|
const where = {
|
|
@@ -952,6 +1079,7 @@ var SysMetadataRepository = class {
|
|
|
952
1079
|
ref: full,
|
|
953
1080
|
hash: row.checksum ?? null,
|
|
954
1081
|
parentHash: row.previous_checksum ?? null,
|
|
1082
|
+
version: typeof row.version === "number" ? row.version : void 0,
|
|
955
1083
|
actor: row.recorded_by ?? "unknown",
|
|
956
1084
|
message: row.change_note ?? void 0,
|
|
957
1085
|
ts: row.recorded_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
@@ -1033,29 +1161,52 @@ var SysMetadataRepository = class {
|
|
|
1033
1161
|
assertOpen() {
|
|
1034
1162
|
if (this.closed) throw new Error("SysMetadataRepository is closed");
|
|
1035
1163
|
}
|
|
1036
|
-
|
|
1164
|
+
/**
|
|
1165
|
+
* Defense-in-depth authorization gate.
|
|
1166
|
+
*
|
|
1167
|
+
* `intent` defaults to `'override-artifact'` (the historical strict
|
|
1168
|
+
* behavior). The protocol layer passes `'runtime-only'` after it has
|
|
1169
|
+
* verified — via the schema registry — that no artifact item exists
|
|
1170
|
+
* at `(type, name)`. In that case we accept types with
|
|
1171
|
+
* `allowRuntimeCreate: true`, even when `allowOrgOverride` is false.
|
|
1172
|
+
*
|
|
1173
|
+
* The env-var escape hatch (`OBJECTSTACK_METADATA_WRITABLE`) still
|
|
1174
|
+
* applies to BOTH intents, so operators can opt into artifact
|
|
1175
|
+
* overrides at runtime for emergency fixes.
|
|
1176
|
+
*/
|
|
1177
|
+
assertAllowed(type, intent = "override-artifact") {
|
|
1037
1178
|
const singular = PLURAL_TO_SINGULAR[type] ?? type;
|
|
1038
1179
|
const allowedByRegistry = OVERLAY_ALLOWED_TYPES.has(singular) || OVERLAY_ALLOWED_TYPES.has(type);
|
|
1039
1180
|
if (allowedByRegistry) return;
|
|
1181
|
+
if (intent === "runtime-only") {
|
|
1182
|
+
if (RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (!STATIC_REGISTRY_TYPES.has(singular) && !STATIC_REGISTRY_TYPES.has(type)) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1040
1189
|
const env = envWritableMetadataTypes();
|
|
1041
1190
|
if (env.has(singular) || env.has(type)) return;
|
|
1042
1191
|
const allowed = [
|
|
1043
1192
|
...OVERLAY_ALLOWED_TYPES,
|
|
1044
1193
|
...envWritableMetadataTypes()
|
|
1045
1194
|
];
|
|
1195
|
+
const code = intent === "runtime-only" ? "not_creatable" : "not_overridable";
|
|
1196
|
+
const detail = intent === "runtime-only" ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `;
|
|
1046
1197
|
const err = new Error(
|
|
1047
|
-
`[
|
|
1198
|
+
`[${code}] ${detail}Overlay-allowed: ${Array.from(new Set(allowed)).join(", ") || "(none)"}. Set OBJECTSTACK_METADATA_WRITABLE to enable additional types at runtime.`
|
|
1048
1199
|
);
|
|
1049
|
-
err.code =
|
|
1200
|
+
err.code = code;
|
|
1050
1201
|
err.status = 403;
|
|
1051
1202
|
throw err;
|
|
1052
1203
|
}
|
|
1053
|
-
whereFor(ref) {
|
|
1204
|
+
whereFor(ref, state = "active") {
|
|
1054
1205
|
return {
|
|
1055
1206
|
type: ref.type,
|
|
1056
1207
|
name: ref.name,
|
|
1057
1208
|
organization_id: this.organizationId,
|
|
1058
|
-
state
|
|
1209
|
+
state
|
|
1059
1210
|
};
|
|
1060
1211
|
}
|
|
1061
1212
|
fullRef(ref) {
|
|
@@ -1151,37 +1302,58 @@ var SysMetadataRepository = class {
|
|
|
1151
1302
|
|
|
1152
1303
|
// src/protocol.ts
|
|
1153
1304
|
import { ConflictError as ConflictError2 } from "@objectstack/metadata-core";
|
|
1154
|
-
import { parseFilterAST, isFilterAST
|
|
1155
|
-
import { PLURAL_TO_SINGULAR as
|
|
1156
|
-
import {
|
|
1157
|
-
import {
|
|
1158
|
-
import { PermissionSetSchema } from "@objectstack/spec/security";
|
|
1159
|
-
import { EmailTemplateSchema, JobSchema, METADATA_FORM_REGISTRY } from "@objectstack/spec/system";
|
|
1160
|
-
import { ToolSchema, SkillSchema, AgentSchema } from "@objectstack/spec/ai";
|
|
1161
|
-
import { FlowSchema, WorkflowRuleSchema, ApprovalProcessSchema } from "@objectstack/spec/automation";
|
|
1162
|
-
import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2 } from "@objectstack/spec/kernel";
|
|
1305
|
+
import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
|
|
1306
|
+
import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR3, SINGULAR_TO_PLURAL as SINGULAR_TO_PLURAL2 } from "@objectstack/spec/shared";
|
|
1307
|
+
import { METADATA_FORM_REGISTRY } from "@objectstack/spec/system";
|
|
1308
|
+
import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2 } from "@objectstack/spec/kernel";
|
|
1163
1309
|
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
|
-
|
|
1310
|
+
|
|
1311
|
+
// src/metadata-diagnostics.ts
|
|
1312
|
+
import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
|
|
1313
|
+
import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR2 } from "@objectstack/spec/shared";
|
|
1314
|
+
function computeMetadataDiagnostics(type, item) {
|
|
1315
|
+
const singular = PLURAL_TO_SINGULAR2[type] ?? type;
|
|
1316
|
+
const schema = getMetadataTypeSchema(singular);
|
|
1317
|
+
if (!schema) return void 0;
|
|
1318
|
+
if (item === null || item === void 0 || typeof item !== "object") {
|
|
1319
|
+
return {
|
|
1320
|
+
valid: false,
|
|
1321
|
+
errors: [{
|
|
1322
|
+
path: "",
|
|
1323
|
+
message: "Metadata document must be a non-null object",
|
|
1324
|
+
code: "invalid_type"
|
|
1325
|
+
}]
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
const candidate = "_diagnostics" in item ? stripDiagnostics(item) : item;
|
|
1329
|
+
const parsed = schema.safeParse(candidate);
|
|
1330
|
+
if (parsed.success) {
|
|
1331
|
+
return { valid: true };
|
|
1332
|
+
}
|
|
1333
|
+
const errors = parsed.error.issues.map((issue) => ({
|
|
1334
|
+
path: issue.path.map(String).join("."),
|
|
1335
|
+
message: issue.message,
|
|
1336
|
+
code: issue.code
|
|
1337
|
+
}));
|
|
1338
|
+
return { valid: false, errors };
|
|
1339
|
+
}
|
|
1340
|
+
function stripDiagnostics(item) {
|
|
1341
|
+
const { _diagnostics: _drop, ...rest } = item;
|
|
1342
|
+
void _drop;
|
|
1343
|
+
return rest;
|
|
1344
|
+
}
|
|
1345
|
+
function decorateMetadataItem(type, item) {
|
|
1346
|
+
if (!item || typeof item !== "object") return item;
|
|
1347
|
+
const diagnostics = computeMetadataDiagnostics(type, item);
|
|
1348
|
+
if (!diagnostics) return item;
|
|
1349
|
+
return { ...item, _diagnostics: diagnostics };
|
|
1350
|
+
}
|
|
1351
|
+
function decorateMetadataItems(type, items) {
|
|
1352
|
+
if (!Array.isArray(items)) return items;
|
|
1353
|
+
return items.map((item) => decorateMetadataItem(type, item));
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/protocol.ts
|
|
1185
1357
|
var TYPE_TO_FORM = METADATA_FORM_REGISTRY;
|
|
1186
1358
|
var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
1187
1359
|
function toJsonSchemaSafe(schema) {
|
|
@@ -1211,9 +1383,17 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1211
1383
|
abstract: { type: "boolean", default: false },
|
|
1212
1384
|
datasource: { type: "string" },
|
|
1213
1385
|
fields: {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1386
|
+
// Canonical Object.fields is a name-keyed map
|
|
1387
|
+
// (Record<string, FieldDefinition>) — insertion order is
|
|
1388
|
+
// display order. The SchemaForm engine recognises
|
|
1389
|
+
// `additionalProperties` as a Record and dispatches to
|
|
1390
|
+
// the `record` form-field renderer (ADR-0007). The form
|
|
1391
|
+
// layout in `object.form.ts` declares `type: 'record'`
|
|
1392
|
+
// so the inner `additionalProperties` schema is used to
|
|
1393
|
+
// shape each value.
|
|
1394
|
+
type: "object",
|
|
1395
|
+
default: {},
|
|
1396
|
+
additionalProperties: {
|
|
1217
1397
|
type: "object",
|
|
1218
1398
|
properties: {
|
|
1219
1399
|
name: { type: "string" },
|
|
@@ -1224,7 +1404,7 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1224
1404
|
defaultValue: {},
|
|
1225
1405
|
description: { type: "string" }
|
|
1226
1406
|
},
|
|
1227
|
-
required: ["
|
|
1407
|
+
required: ["type"]
|
|
1228
1408
|
}
|
|
1229
1409
|
},
|
|
1230
1410
|
capabilities: { type: "object", additionalProperties: true }
|
|
@@ -1369,19 +1549,9 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1369
1549
|
additionalProperties: true
|
|
1370
1550
|
}
|
|
1371
1551
|
};
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
switch (singular) {
|
|
1376
|
-
case "view": {
|
|
1377
|
-
const t = item && typeof item === "object" && "type" in item ? String(item.type) : void 0;
|
|
1378
|
-
return t && FORM_VIEW_TYPES.has(t) ? FormViewSchema : ListViewSchema;
|
|
1379
|
-
}
|
|
1380
|
-
case "dashboard":
|
|
1381
|
-
return DashboardSchema;
|
|
1382
|
-
default:
|
|
1383
|
-
return null;
|
|
1384
|
-
}
|
|
1552
|
+
function resolveOverlaySchema(type, _item) {
|
|
1553
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
1554
|
+
return getMetadataTypeSchema2(singular) ?? null;
|
|
1385
1555
|
}
|
|
1386
1556
|
function simpleHash(str) {
|
|
1387
1557
|
let hash = 0;
|
|
@@ -1514,6 +1684,32 @@ function extractPathValues(item, path) {
|
|
|
1514
1684
|
}
|
|
1515
1685
|
return out;
|
|
1516
1686
|
}
|
|
1687
|
+
function diffShallow(from, to) {
|
|
1688
|
+
const added = [];
|
|
1689
|
+
const removed = [];
|
|
1690
|
+
const changed = [];
|
|
1691
|
+
const fromKeys = new Set(Object.keys(from ?? {}));
|
|
1692
|
+
const toKeys = new Set(Object.keys(to ?? {}));
|
|
1693
|
+
for (const k of toKeys) {
|
|
1694
|
+
if (!fromKeys.has(k)) {
|
|
1695
|
+
added.push({ path: k, value: to[k] });
|
|
1696
|
+
} else {
|
|
1697
|
+
const a = from[k];
|
|
1698
|
+
const b = to[k];
|
|
1699
|
+
const aStr = JSON.stringify(a);
|
|
1700
|
+
const bStr = JSON.stringify(b);
|
|
1701
|
+
if (aStr !== bStr) {
|
|
1702
|
+
changed.push({ path: k, from: a, to: b });
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
for (const k of fromKeys) {
|
|
1707
|
+
if (!toKeys.has(k)) {
|
|
1708
|
+
removed.push({ path: k, value: from[k] });
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
return { added, removed, changed };
|
|
1712
|
+
}
|
|
1517
1713
|
function detectDestructiveObjectChanges(prev, next) {
|
|
1518
1714
|
if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
|
|
1519
1715
|
const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
|
|
@@ -1645,6 +1841,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1645
1841
|
}
|
|
1646
1842
|
}
|
|
1647
1843
|
}
|
|
1844
|
+
const draftPartialSql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id) WHERE state = 'draft'";
|
|
1845
|
+
try {
|
|
1846
|
+
await exec(draftPartialSql);
|
|
1847
|
+
} catch (err) {
|
|
1848
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1849
|
+
if (/partial|where clause|syntax/i.test(msg)) {
|
|
1850
|
+
try {
|
|
1851
|
+
await exec(
|
|
1852
|
+
"CREATE INDEX IF NOT EXISTS idx_sys_metadata_overlay_draft ON sys_metadata (type, name, organization_id)"
|
|
1853
|
+
);
|
|
1854
|
+
} catch {
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1648
1858
|
} catch {
|
|
1649
1859
|
}
|
|
1650
1860
|
}
|
|
@@ -1768,8 +1978,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1768
1978
|
DEFAULT_METADATA_TYPE_REGISTRY2.map((e) => [e.type, e])
|
|
1769
1979
|
);
|
|
1770
1980
|
const entries = allTypes.map((type) => {
|
|
1771
|
-
const singular =
|
|
1772
|
-
const zodSchema = singular
|
|
1981
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
1982
|
+
const zodSchema = getMetadataTypeSchema2(singular);
|
|
1773
1983
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
1774
1984
|
const form = TYPE_TO_FORM[singular];
|
|
1775
1985
|
const base = registryByType.get(singular);
|
|
@@ -1810,19 +2020,76 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1810
2020
|
});
|
|
1811
2021
|
return { types: allTypes, entries };
|
|
1812
2022
|
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Sweep all (or filtered) metadata types and report entries that
|
|
2025
|
+
* fail spec validation. Powers the Studio governance view
|
|
2026
|
+
* (`GET /api/v1/meta/diagnostics`) and `os doctor`-style CLI
|
|
2027
|
+
* checks.
|
|
2028
|
+
*
|
|
2029
|
+
* `severity` defaults to `'error'` — only entries with at least
|
|
2030
|
+
* one Zod error issue are returned. `'warning'` includes
|
|
2031
|
+
* everything we surface (warnings are reserved for a future lint
|
|
2032
|
+
* layer on top of spec validation).
|
|
2033
|
+
*
|
|
2034
|
+
* `type` may be either a singular (`'view'`) or plural (`'views'`)
|
|
2035
|
+
* identifier; the underlying `getMetaItems` already normalises.
|
|
2036
|
+
*
|
|
2037
|
+
* Implementation note: leverages the `_diagnostics` already
|
|
2038
|
+
* decorated onto items by `getMetaItems()` to avoid running
|
|
2039
|
+
* `safeParse()` twice. For types whose schema is unregistered we
|
|
2040
|
+
* skip silently (they cannot be validated and should not appear
|
|
2041
|
+
* as "valid" either — they are simply opaque to this report).
|
|
2042
|
+
*/
|
|
2043
|
+
async getMetaDiagnostics(request = {}) {
|
|
2044
|
+
const includeWarnings = request.severity === "warning";
|
|
2045
|
+
const targetTypes = request.type ? [request.type] : DEFAULT_METADATA_TYPE_REGISTRY2.filter((e) => getMetadataTypeSchema2(e.type)).map((e) => e.type);
|
|
2046
|
+
const entries = [];
|
|
2047
|
+
let scannedItems = 0;
|
|
2048
|
+
for (const t of targetTypes) {
|
|
2049
|
+
let listed;
|
|
2050
|
+
try {
|
|
2051
|
+
listed = await this.getMetaItems({
|
|
2052
|
+
type: t,
|
|
2053
|
+
organizationId: request.organizationId,
|
|
2054
|
+
packageId: request.packageId
|
|
2055
|
+
});
|
|
2056
|
+
} catch {
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
const items = Array.isArray(listed?.items) ? listed.items : Array.isArray(listed) ? listed : [];
|
|
2060
|
+
for (const item of items) {
|
|
2061
|
+
scannedItems += 1;
|
|
2062
|
+
const diag = item?._diagnostics ?? computeMetadataDiagnostics(t, item);
|
|
2063
|
+
if (!diag) continue;
|
|
2064
|
+
if (diag.valid && !includeWarnings) continue;
|
|
2065
|
+
if (diag.valid && includeWarnings && !diag.warnings?.length) continue;
|
|
2066
|
+
entries.push({
|
|
2067
|
+
type: t,
|
|
2068
|
+
name: typeof item?.name === "string" ? item.name : "<unknown>",
|
|
2069
|
+
diagnostics: diag
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
entries,
|
|
2075
|
+
total: entries.length,
|
|
2076
|
+
scannedTypes: targetTypes.length,
|
|
2077
|
+
scannedItems
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
1813
2080
|
async getMetaItems(request) {
|
|
1814
2081
|
const { packageId } = request;
|
|
1815
2082
|
let items = [];
|
|
1816
2083
|
if (this.environmentId === void 0) {
|
|
1817
2084
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1818
2085
|
if (items.length === 0) {
|
|
1819
|
-
const alt =
|
|
2086
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1820
2087
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1821
2088
|
}
|
|
1822
2089
|
} else {
|
|
1823
2090
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1824
2091
|
if (items.length === 0) {
|
|
1825
|
-
const alt =
|
|
2092
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1826
2093
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1827
2094
|
}
|
|
1828
2095
|
}
|
|
@@ -1837,7 +2104,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1837
2104
|
if (packageId) whereClause._packageId = packageId;
|
|
1838
2105
|
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
1839
2106
|
if (!rs || rs.length === 0) {
|
|
1840
|
-
const alt =
|
|
2107
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1841
2108
|
if (alt) {
|
|
1842
2109
|
const altWhere = { type: alt, state: "active", organization_id: oid };
|
|
1843
2110
|
if (packageId) altWhere._packageId = packageId;
|
|
@@ -1904,28 +2171,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1904
2171
|
}
|
|
1905
2172
|
return {
|
|
1906
2173
|
type: request.type,
|
|
1907
|
-
items
|
|
2174
|
+
items: decorateMetadataItems(request.type, items)
|
|
1908
2175
|
};
|
|
1909
2176
|
}
|
|
1910
2177
|
async getMetaItem(request) {
|
|
1911
2178
|
let item;
|
|
1912
2179
|
const orgId = request.organizationId;
|
|
2180
|
+
const readState = request.state === "draft" ? "draft" : "active";
|
|
1913
2181
|
try {
|
|
1914
2182
|
const findOverlay = async (oid) => {
|
|
1915
2183
|
const where = {
|
|
1916
2184
|
type: request.type,
|
|
1917
2185
|
name: request.name,
|
|
1918
|
-
state:
|
|
2186
|
+
state: readState,
|
|
1919
2187
|
organization_id: oid
|
|
1920
2188
|
};
|
|
1921
2189
|
const rec = await this.engine.findOne("sys_metadata", { where });
|
|
1922
2190
|
if (rec) return rec;
|
|
1923
|
-
const alt =
|
|
2191
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1924
2192
|
if (alt) {
|
|
1925
2193
|
const altWhere = {
|
|
1926
2194
|
type: alt,
|
|
1927
2195
|
name: request.name,
|
|
1928
|
-
state:
|
|
2196
|
+
state: readState,
|
|
1929
2197
|
organization_id: oid
|
|
1930
2198
|
};
|
|
1931
2199
|
return await this.engine.findOne("sys_metadata", { where: altWhere });
|
|
@@ -1938,6 +2206,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1938
2206
|
}
|
|
1939
2207
|
} catch {
|
|
1940
2208
|
}
|
|
2209
|
+
if (readState === "draft") {
|
|
2210
|
+
if (item === void 0) {
|
|
2211
|
+
const err = new Error(
|
|
2212
|
+
`[no_draft] No pending draft exists for ${request.type}/${request.name}.`
|
|
2213
|
+
);
|
|
2214
|
+
err.code = "no_draft";
|
|
2215
|
+
err.status = 404;
|
|
2216
|
+
throw err;
|
|
2217
|
+
}
|
|
2218
|
+
return { type: request.type, name: request.name, item: decorateMetadataItem(request.type, item) };
|
|
2219
|
+
}
|
|
1941
2220
|
if (item === void 0) {
|
|
1942
2221
|
try {
|
|
1943
2222
|
const services = this.getServicesRegistry?.();
|
|
@@ -1947,7 +2226,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1947
2226
|
if (fromService !== void 0 && fromService !== null) {
|
|
1948
2227
|
item = fromService;
|
|
1949
2228
|
} else {
|
|
1950
|
-
const alt =
|
|
2229
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1951
2230
|
if (alt) {
|
|
1952
2231
|
const altFromService = await metadataService.get(alt, request.name);
|
|
1953
2232
|
if (altFromService !== void 0 && altFromService !== null) {
|
|
@@ -1962,14 +2241,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1962
2241
|
if (item === void 0) {
|
|
1963
2242
|
item = this.engine.registry.getItem(request.type, request.name);
|
|
1964
2243
|
if (item === void 0) {
|
|
1965
|
-
const alt =
|
|
2244
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1966
2245
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
1967
2246
|
}
|
|
1968
2247
|
}
|
|
1969
2248
|
return {
|
|
1970
2249
|
type: request.type,
|
|
1971
2250
|
name: request.name,
|
|
1972
|
-
item
|
|
2251
|
+
item: decorateMetadataItem(request.type, item)
|
|
1973
2252
|
};
|
|
1974
2253
|
}
|
|
1975
2254
|
/**
|
|
@@ -1995,7 +2274,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1995
2274
|
if (metadataService && typeof metadataService.get === "function") {
|
|
1996
2275
|
let fromService = await metadataService.get(request.type, request.name);
|
|
1997
2276
|
if (fromService === void 0 || fromService === null) {
|
|
1998
|
-
const alt =
|
|
2277
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1999
2278
|
if (alt) fromService = await metadataService.get(alt, request.name);
|
|
2000
2279
|
}
|
|
2001
2280
|
if (fromService !== void 0 && fromService !== null) code = fromService;
|
|
@@ -2005,7 +2284,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2005
2284
|
if (code === null) {
|
|
2006
2285
|
let regItem = this.engine.registry.getItem(request.type, request.name);
|
|
2007
2286
|
if (regItem === void 0) {
|
|
2008
|
-
const alt =
|
|
2287
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2009
2288
|
if (alt) regItem = this.engine.registry.getItem(alt, request.name);
|
|
2010
2289
|
}
|
|
2011
2290
|
if (regItem !== void 0) code = regItem;
|
|
@@ -2022,7 +2301,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2022
2301
|
};
|
|
2023
2302
|
let rec = await this.engine.findOne("sys_metadata", { where });
|
|
2024
2303
|
if (!rec) {
|
|
2025
|
-
const alt =
|
|
2304
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2026
2305
|
if (alt) {
|
|
2027
2306
|
rec = await this.engine.findOne("sys_metadata", {
|
|
2028
2307
|
where: { ...where, type: alt }
|
|
@@ -2048,13 +2327,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2048
2327
|
} catch {
|
|
2049
2328
|
}
|
|
2050
2329
|
const effective = overlay ?? code;
|
|
2330
|
+
const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
|
|
2051
2331
|
return {
|
|
2052
2332
|
type: request.type,
|
|
2053
2333
|
name: request.name,
|
|
2054
2334
|
code,
|
|
2055
2335
|
overlay,
|
|
2056
2336
|
overlayScope,
|
|
2057
|
-
effective
|
|
2337
|
+
effective,
|
|
2338
|
+
..._diagnostics ? { _diagnostics } : {}
|
|
2058
2339
|
};
|
|
2059
2340
|
}
|
|
2060
2341
|
async getUiView(request) {
|
|
@@ -2935,7 +3216,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2935
3216
|
for (const tok of raw.split(",")) {
|
|
2936
3217
|
const t = tok.trim();
|
|
2937
3218
|
if (!t) continue;
|
|
2938
|
-
const singular =
|
|
3219
|
+
const singular = PLURAL_TO_SINGULAR3[t] ?? t;
|
|
2939
3220
|
set.add(singular);
|
|
2940
3221
|
const plural = SINGULAR_TO_PLURAL2[singular];
|
|
2941
3222
|
if (plural) set.add(plural);
|
|
@@ -2949,13 +3230,49 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2949
3230
|
}
|
|
2950
3231
|
/** Normalize plural→singular before consulting the allow-list. */
|
|
2951
3232
|
static isOverlayAllowed(type) {
|
|
2952
|
-
const singular =
|
|
3233
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
2953
3234
|
if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
|
|
2954
3235
|
return true;
|
|
2955
3236
|
}
|
|
2956
3237
|
const env = this.envWritableTypes();
|
|
2957
3238
|
return env.has(singular) || env.has(type);
|
|
2958
3239
|
}
|
|
3240
|
+
/** Does this type permit creating brand-new (artifact-free) items? */
|
|
3241
|
+
static isRuntimeCreateAllowed(type) {
|
|
3242
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
3243
|
+
if (this.RUNTIME_CREATE_ALLOWED_TYPES.has(singular) || this.RUNTIME_CREATE_ALLOWED_TYPES.has(type)) {
|
|
3244
|
+
return true;
|
|
3245
|
+
}
|
|
3246
|
+
if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) {
|
|
3247
|
+
return true;
|
|
3248
|
+
}
|
|
3249
|
+
return false;
|
|
3250
|
+
}
|
|
3251
|
+
/**
|
|
3252
|
+
* Does an artifact (npm-package-loaded) item exist at `(type, name)`?
|
|
3253
|
+
*
|
|
3254
|
+
* The schema registry's `_packageId` tag is set only when
|
|
3255
|
+
* `registerItem(..., packageId)` is called with a truthy packageId
|
|
3256
|
+
* — and only artifact loaders do that. DB-rehydrated items
|
|
3257
|
+
* (sys_metadata rows registered back into the registry by
|
|
3258
|
+
* `getMetaItems` / `loadMetaFromDb`) call `registerItem` without a
|
|
3259
|
+
* packageId, so they carry no `_packageId` and are correctly
|
|
3260
|
+
* excluded here.
|
|
3261
|
+
*
|
|
3262
|
+
* Used by the two-tier authorization model to distinguish
|
|
3263
|
+
* "overlaying a packaged item" (requires `allowOrgOverride`) from
|
|
3264
|
+
* "authoring a DB-only item" (requires only `allowRuntimeCreate`).
|
|
3265
|
+
*/
|
|
3266
|
+
isArtifactBacked(type, name) {
|
|
3267
|
+
const registry = this.engine?.registry;
|
|
3268
|
+
if (!registry || typeof registry.getItem !== "function") {
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
3272
|
+
const item = registry.getItem(singular, name) ?? registry.getItem(type, name);
|
|
3273
|
+
if (!item || !item._packageId) return false;
|
|
3274
|
+
return item._packageId !== "sys_metadata";
|
|
3275
|
+
}
|
|
2959
3276
|
/**
|
|
2960
3277
|
* Mirror an object-type overlay write into the in-memory engine
|
|
2961
3278
|
* registry so subsequent CRUD finds the new schema. Idempotent and
|
|
@@ -2981,16 +3298,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2981
3298
|
if (!request.item) {
|
|
2982
3299
|
throw new Error("Item data is required");
|
|
2983
3300
|
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
);
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3301
|
+
const mode = request.mode === "draft" ? "draft" : "publish";
|
|
3302
|
+
if (this.environmentId !== void 0) {
|
|
3303
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
3304
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
3305
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
3306
|
+
if (artifactBacked && !overlayAllowed) {
|
|
3307
|
+
const err = new Error(
|
|
3308
|
+
`[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.`
|
|
3309
|
+
);
|
|
3310
|
+
err.code = "not_overridable";
|
|
3311
|
+
err.status = 403;
|
|
3312
|
+
throw err;
|
|
3313
|
+
}
|
|
3314
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
3315
|
+
const err = new Error(
|
|
3316
|
+
`[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.`
|
|
3317
|
+
);
|
|
3318
|
+
err.code = "not_creatable";
|
|
3319
|
+
err.status = 403;
|
|
3320
|
+
throw err;
|
|
3321
|
+
}
|
|
2992
3322
|
}
|
|
2993
|
-
const singularType =
|
|
3323
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
2994
3324
|
if (!request.force && (singularType === "object" || singularType === "field")) {
|
|
2995
3325
|
try {
|
|
2996
3326
|
const existing = await this.getMetaItem({
|
|
@@ -3038,8 +3368,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3038
3368
|
}
|
|
3039
3369
|
}
|
|
3040
3370
|
await this.ensureOverlayIndex();
|
|
3041
|
-
const singularTypeForRepo =
|
|
3042
|
-
|
|
3371
|
+
const singularTypeForRepo = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3372
|
+
const overlayAllowedForRepo = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
3373
|
+
const runtimeCreateAllowedForRepo = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
3374
|
+
const useRepoPath = overlayAllowedForRepo || runtimeCreateAllowedForRepo;
|
|
3375
|
+
if (useRepoPath) {
|
|
3376
|
+
const artifactBacked = this.isArtifactBacked(singularTypeForRepo, request.name);
|
|
3377
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3043
3378
|
const orgId = request.organizationId ?? null;
|
|
3044
3379
|
const repo = this.getOverlayRepo(orgId);
|
|
3045
3380
|
const ref = {
|
|
@@ -3051,21 +3386,26 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3051
3386
|
if (request.parentVersion !== void 0) {
|
|
3052
3387
|
parentVersion = request.parentVersion;
|
|
3053
3388
|
} else {
|
|
3054
|
-
const current = await repo.get(ref);
|
|
3389
|
+
const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
|
|
3055
3390
|
parentVersion = current?.hash ?? null;
|
|
3056
3391
|
}
|
|
3057
3392
|
try {
|
|
3058
3393
|
const result = await repo.put(ref, request.item, {
|
|
3059
3394
|
parentVersion,
|
|
3060
3395
|
actor: request.actor ?? "system",
|
|
3061
|
-
source: "protocol.saveMetaItem"
|
|
3396
|
+
source: "protocol.saveMetaItem",
|
|
3397
|
+
intent,
|
|
3398
|
+
state: mode === "draft" ? "draft" : "active"
|
|
3062
3399
|
});
|
|
3063
|
-
|
|
3400
|
+
if (mode === "publish") {
|
|
3401
|
+
this.applyObjectRegistryMutation(request);
|
|
3402
|
+
}
|
|
3064
3403
|
return {
|
|
3065
3404
|
success: true,
|
|
3066
3405
|
version: result.version,
|
|
3067
3406
|
seq: result.seq,
|
|
3068
|
-
|
|
3407
|
+
state: mode === "draft" ? "draft" : "active",
|
|
3408
|
+
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
3409
|
};
|
|
3070
3410
|
} catch (err) {
|
|
3071
3411
|
if (err instanceof ConflictError2) {
|
|
@@ -3149,8 +3489,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3149
3489
|
* "no history" uniformly.
|
|
3150
3490
|
*/
|
|
3151
3491
|
async historyMetaItem(request) {
|
|
3152
|
-
const singularType =
|
|
3153
|
-
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
|
|
3492
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3493
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3154
3494
|
return { events: [] };
|
|
3155
3495
|
}
|
|
3156
3496
|
const orgId = request.organizationId ?? null;
|
|
@@ -3168,22 +3508,238 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3168
3508
|
return { events };
|
|
3169
3509
|
}
|
|
3170
3510
|
/**
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3173
|
-
*
|
|
3174
|
-
* with {@link saveMetaItem}.
|
|
3511
|
+
* Promote the pending draft overlay to the live (`active`) row.
|
|
3512
|
+
* Records a history event with `op='publish'`. 404 (`[no_draft]`)
|
|
3513
|
+
* when there is nothing to publish.
|
|
3175
3514
|
*/
|
|
3176
|
-
async
|
|
3177
|
-
|
|
3515
|
+
async publishMetaItem(request) {
|
|
3516
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3517
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3518
|
+
const err = new Error(
|
|
3519
|
+
`[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
|
|
3520
|
+
);
|
|
3521
|
+
err.code = "not_overridable";
|
|
3522
|
+
err.status = 403;
|
|
3523
|
+
throw err;
|
|
3524
|
+
}
|
|
3525
|
+
await this.ensureOverlayIndex();
|
|
3526
|
+
const orgId = request.organizationId ?? null;
|
|
3527
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3528
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3529
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3530
|
+
const ref = {
|
|
3531
|
+
type: singularType,
|
|
3532
|
+
name: request.name,
|
|
3533
|
+
org: orgId ?? "env"
|
|
3534
|
+
};
|
|
3535
|
+
try {
|
|
3536
|
+
const result = await repo.promoteDraft(ref, {
|
|
3537
|
+
actor: request.actor ?? "system",
|
|
3538
|
+
source: "protocol.publishMetaItem",
|
|
3539
|
+
...request.message ? { message: request.message } : {},
|
|
3540
|
+
intent
|
|
3541
|
+
});
|
|
3542
|
+
this.applyObjectRegistryMutation({
|
|
3543
|
+
type: request.type,
|
|
3544
|
+
name: request.name,
|
|
3545
|
+
item: result.item.body
|
|
3546
|
+
});
|
|
3547
|
+
return {
|
|
3548
|
+
success: true,
|
|
3549
|
+
version: result.version,
|
|
3550
|
+
seq: result.seq,
|
|
3551
|
+
message: `Published draft \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3552
|
+
};
|
|
3553
|
+
} catch (err) {
|
|
3554
|
+
if (err instanceof ConflictError2) {
|
|
3555
|
+
const conflict = new Error(
|
|
3556
|
+
`[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"}.`
|
|
3557
|
+
);
|
|
3558
|
+
conflict.code = "metadata_conflict";
|
|
3559
|
+
conflict.status = 409;
|
|
3560
|
+
conflict.expectedParent = err.expectedParent;
|
|
3561
|
+
conflict.actualHead = err.actualHead;
|
|
3562
|
+
throw conflict;
|
|
3563
|
+
}
|
|
3564
|
+
throw err;
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
/**
|
|
3568
|
+
* Restore the body recorded at history `toVersion` as the new
|
|
3569
|
+
* live row. Writes a history event with `op='revert'`. 404
|
|
3570
|
+
* (`[version_not_found]`) when the target version doesn't exist;
|
|
3571
|
+
* 409 (`[version_not_restorable]`) when the target is a delete
|
|
3572
|
+
* tombstone (no body to bring back).
|
|
3573
|
+
*/
|
|
3574
|
+
async rollbackMetaItem(request) {
|
|
3575
|
+
if (!Number.isFinite(request.toVersion) || request.toVersion < 1) {
|
|
3576
|
+
const err = new Error(
|
|
3577
|
+
`[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`
|
|
3578
|
+
);
|
|
3579
|
+
err.code = "invalid_request";
|
|
3580
|
+
err.status = 400;
|
|
3581
|
+
throw err;
|
|
3582
|
+
}
|
|
3583
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3584
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3178
3585
|
const err = new Error(
|
|
3179
|
-
`[not_overridable] Metadata type '${request.type}'
|
|
3586
|
+
`[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
|
|
3180
3587
|
);
|
|
3181
3588
|
err.code = "not_overridable";
|
|
3182
3589
|
err.status = 403;
|
|
3183
3590
|
throw err;
|
|
3184
3591
|
}
|
|
3185
|
-
|
|
3186
|
-
const
|
|
3592
|
+
await this.ensureOverlayIndex();
|
|
3593
|
+
const orgId = request.organizationId ?? null;
|
|
3594
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3595
|
+
const artifactBacked = this.isArtifactBacked(singularType, request.name);
|
|
3596
|
+
const intent = artifactBacked ? "override-artifact" : "runtime-only";
|
|
3597
|
+
const ref = {
|
|
3598
|
+
type: singularType,
|
|
3599
|
+
name: request.name,
|
|
3600
|
+
org: orgId ?? "env"
|
|
3601
|
+
};
|
|
3602
|
+
try {
|
|
3603
|
+
const result = await repo.restoreVersion(ref, request.toVersion, {
|
|
3604
|
+
actor: request.actor ?? "system",
|
|
3605
|
+
source: "protocol.rollbackMetaItem",
|
|
3606
|
+
...request.message ? { message: request.message } : {},
|
|
3607
|
+
intent
|
|
3608
|
+
});
|
|
3609
|
+
this.applyObjectRegistryMutation({
|
|
3610
|
+
type: request.type,
|
|
3611
|
+
name: request.name,
|
|
3612
|
+
item: result.item.body
|
|
3613
|
+
});
|
|
3614
|
+
return {
|
|
3615
|
+
success: true,
|
|
3616
|
+
version: result.version,
|
|
3617
|
+
seq: result.seq,
|
|
3618
|
+
restoredFromVersion: request.toVersion,
|
|
3619
|
+
message: `Reverted to version ${request.toVersion} \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
3620
|
+
};
|
|
3621
|
+
} catch (err) {
|
|
3622
|
+
if (err instanceof ConflictError2) {
|
|
3623
|
+
const conflict = new Error(
|
|
3624
|
+
`[metadata_conflict] ${request.type}/${request.name} advanced during rollback. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
|
|
3625
|
+
);
|
|
3626
|
+
conflict.code = "metadata_conflict";
|
|
3627
|
+
conflict.status = 409;
|
|
3628
|
+
conflict.expectedParent = err.expectedParent;
|
|
3629
|
+
conflict.actualHead = err.actualHead;
|
|
3630
|
+
throw conflict;
|
|
3631
|
+
}
|
|
3632
|
+
throw err;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
/**
|
|
3636
|
+
* Compute a shallow structural diff between two historical
|
|
3637
|
+
* versions of a metadata item. Either side may be omitted: when
|
|
3638
|
+
* `toVersion` is undefined the current active body is used; when
|
|
3639
|
+
* `fromVersion` is undefined the immediately previous history row
|
|
3640
|
+
* is used. Returns `{ added, removed, changed }` keyed by JSON
|
|
3641
|
+
* pointer-style paths for primitive leaves; nested objects/arrays
|
|
3642
|
+
* are reported as a single change record.
|
|
3643
|
+
*/
|
|
3644
|
+
async diffMetaItem(request) {
|
|
3645
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3646
|
+
const orgId = request.organizationId ?? null;
|
|
3647
|
+
const events = (await this.historyMetaItem({
|
|
3648
|
+
type: singularType,
|
|
3649
|
+
name: request.name,
|
|
3650
|
+
...orgId ? { organizationId: orgId } : {}
|
|
3651
|
+
})).events;
|
|
3652
|
+
const versions = events.map((ev) => ev.version).filter((v) => typeof v === "number");
|
|
3653
|
+
const repo = this.getOverlayRepo(orgId);
|
|
3654
|
+
const fullRef = {
|
|
3655
|
+
type: singularType,
|
|
3656
|
+
name: request.name,
|
|
3657
|
+
org: orgId ?? "env"
|
|
3658
|
+
};
|
|
3659
|
+
const histRows = [];
|
|
3660
|
+
try {
|
|
3661
|
+
const engineAny = this.engine;
|
|
3662
|
+
const rows = await engineAny.find("sys_metadata_history", {
|
|
3663
|
+
where: {
|
|
3664
|
+
organization_id: orgId,
|
|
3665
|
+
type: singularType,
|
|
3666
|
+
name: request.name
|
|
3667
|
+
}
|
|
3668
|
+
});
|
|
3669
|
+
rows.sort((a, b) => (a.version ?? 0) - (b.version ?? 0));
|
|
3670
|
+
for (const r of rows) {
|
|
3671
|
+
const body = r.metadata == null ? null : typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
3672
|
+
histRows.push({ version: r.version ?? 0, body });
|
|
3673
|
+
}
|
|
3674
|
+
} catch {
|
|
3675
|
+
}
|
|
3676
|
+
const byVersion = /* @__PURE__ */ new Map();
|
|
3677
|
+
for (const r of histRows) byVersion.set(r.version, r.body);
|
|
3678
|
+
let fromBody = null;
|
|
3679
|
+
let toBody = null;
|
|
3680
|
+
let fromVersion = null;
|
|
3681
|
+
let toVersion = null;
|
|
3682
|
+
if (request.toVersion !== void 0) {
|
|
3683
|
+
toVersion = request.toVersion;
|
|
3684
|
+
toBody = byVersion.get(request.toVersion) ?? null;
|
|
3685
|
+
} else {
|
|
3686
|
+
const current = await repo.get(fullRef, { state: "active" });
|
|
3687
|
+
toBody = current ? current.body : null;
|
|
3688
|
+
toVersion = histRows.length ? histRows[histRows.length - 1].version : null;
|
|
3689
|
+
}
|
|
3690
|
+
if (request.fromVersion !== void 0) {
|
|
3691
|
+
fromVersion = request.fromVersion;
|
|
3692
|
+
fromBody = byVersion.get(request.fromVersion) ?? null;
|
|
3693
|
+
} else if (toVersion !== null) {
|
|
3694
|
+
const sorted = histRows.map((r) => r.version).filter((v) => v < toVersion);
|
|
3695
|
+
if (sorted.length) {
|
|
3696
|
+
fromVersion = sorted[sorted.length - 1];
|
|
3697
|
+
fromBody = byVersion.get(fromVersion) ?? null;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
const diff = diffShallow(fromBody ?? {}, toBody ?? {});
|
|
3701
|
+
const _used = versions;
|
|
3702
|
+
void _used;
|
|
3703
|
+
return {
|
|
3704
|
+
type: request.type,
|
|
3705
|
+
name: request.name,
|
|
3706
|
+
fromVersion,
|
|
3707
|
+
toVersion,
|
|
3708
|
+
...diff
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
3711
|
+
/**
|
|
3712
|
+
* Remove a customization overlay row for the given metadata item, so the
|
|
3713
|
+
* next read falls through to the artifact-loaded default. Implements the
|
|
3714
|
+
* "Reset to factory default" semantic from ADR-0005. Whitelist is shared
|
|
3715
|
+
* with {@link saveMetaItem}.
|
|
3716
|
+
*/
|
|
3717
|
+
async deleteMetaItem(request) {
|
|
3718
|
+
if (this.environmentId !== void 0) {
|
|
3719
|
+
const overlayAllowed = _ObjectStackProtocolImplementation.isOverlayAllowed(request.type);
|
|
3720
|
+
const runtimeCreateAllowed = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(request.type);
|
|
3721
|
+
const artifactBacked = this.isArtifactBacked(request.type, request.name);
|
|
3722
|
+
if (artifactBacked && !overlayAllowed) {
|
|
3723
|
+
const err = new Error(
|
|
3724
|
+
`[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.`
|
|
3725
|
+
);
|
|
3726
|
+
err.code = "not_overridable";
|
|
3727
|
+
err.status = 403;
|
|
3728
|
+
throw err;
|
|
3729
|
+
}
|
|
3730
|
+
if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) {
|
|
3731
|
+
const err = new Error(
|
|
3732
|
+
`[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.`
|
|
3733
|
+
);
|
|
3734
|
+
err.code = "not_creatable";
|
|
3735
|
+
err.status = 403;
|
|
3736
|
+
throw err;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
const singularTypeForRepo = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3740
|
+
const overlayAllowedForRepoDel = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
3741
|
+
const runtimeCreateAllowedForRepoDel = _ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularTypeForRepo);
|
|
3742
|
+
const useRepoPath = overlayAllowedForRepoDel || runtimeCreateAllowedForRepoDel;
|
|
3187
3743
|
if (useRepoPath) {
|
|
3188
3744
|
const orgId = request.organizationId ?? null;
|
|
3189
3745
|
const repo = this.getOverlayRepo(orgId);
|
|
@@ -3193,19 +3749,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3193
3749
|
org: orgId ?? "env"
|
|
3194
3750
|
};
|
|
3195
3751
|
try {
|
|
3196
|
-
const
|
|
3752
|
+
const targetState = request.state === "draft" ? "draft" : "active";
|
|
3753
|
+
const current = await repo.get(ref, { state: targetState });
|
|
3197
3754
|
if (!current) {
|
|
3198
3755
|
return {
|
|
3199
3756
|
success: true,
|
|
3200
3757
|
reset: false,
|
|
3201
|
-
message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
|
|
3758
|
+
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
3759
|
};
|
|
3203
3760
|
}
|
|
3204
3761
|
const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
|
|
3205
3762
|
const result = await repo.delete(ref, {
|
|
3206
3763
|
parentVersion,
|
|
3207
3764
|
actor: request.actor ?? "system",
|
|
3208
|
-
source: "protocol.deleteMetaItem"
|
|
3765
|
+
source: "protocol.deleteMetaItem",
|
|
3766
|
+
intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
|
|
3767
|
+
state: targetState
|
|
3209
3768
|
});
|
|
3210
3769
|
if (this.environmentId === void 0) {
|
|
3211
3770
|
try {
|
|
@@ -3224,7 +3783,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3224
3783
|
success: true,
|
|
3225
3784
|
reset: true,
|
|
3226
3785
|
seq: result.seq,
|
|
3227
|
-
message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
|
|
3786
|
+
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
3787
|
};
|
|
3229
3788
|
} catch (err) {
|
|
3230
3789
|
if (err instanceof ConflictError2) {
|
|
@@ -3302,7 +3861,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3302
3861
|
for (const record of records) {
|
|
3303
3862
|
try {
|
|
3304
3863
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
3305
|
-
const normalizedType =
|
|
3864
|
+
const normalizedType = PLURAL_TO_SINGULAR3[record.type] ?? record.type;
|
|
3306
3865
|
if (normalizedType === "object") {
|
|
3307
3866
|
this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
|
|
3308
3867
|
} else {
|
|
@@ -3336,7 +3895,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3336
3895
|
* — the engine never throws.
|
|
3337
3896
|
*/
|
|
3338
3897
|
async findReferencesToMeta(request) {
|
|
3339
|
-
const singularTarget =
|
|
3898
|
+
const singularTarget = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3340
3899
|
const targetName = request.name;
|
|
3341
3900
|
const matchers = REFERENCE_PATHS[singularTarget];
|
|
3342
3901
|
if (!matchers || matchers.length === 0) {
|
|
@@ -3547,6 +4106,41 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
|
3547
4106
|
* {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
|
|
3548
4107
|
*/
|
|
3549
4108
|
_ObjectStackProtocolImplementation._envWritableTypes = null;
|
|
4109
|
+
/**
|
|
4110
|
+
* Types that opt into runtime creation of brand-new items (ADR-0005
|
|
4111
|
+
* extension — two-tier model). A type may have
|
|
4112
|
+
* `allowOrgOverride: false` (cannot overlay artifact-shipped items)
|
|
4113
|
+
* yet still set `allowRuntimeCreate: true` (users can author new
|
|
4114
|
+
* items in `sys_metadata`). The two flags are orthogonal; see
|
|
4115
|
+
* {@link isArtifactBacked} for how the protocol decides which gate
|
|
4116
|
+
* applies to a given save/delete.
|
|
4117
|
+
*/
|
|
4118
|
+
/**
|
|
4119
|
+
* Set of type names that have a static entry in
|
|
4120
|
+
* `DEFAULT_METADATA_TYPE_REGISTRY`. Anything outside this set is
|
|
4121
|
+
* runtime-registered (plugin-provided types like `theme`, `api`,
|
|
4122
|
+
* `connector`) — the listing endpoint at `getMetaTypes()` synthesises
|
|
4123
|
+
* those with `allowRuntimeCreate: true`, so this gate must agree.
|
|
4124
|
+
*/
|
|
4125
|
+
_ObjectStackProtocolImplementation.STATIC_REGISTRY_TYPES = (() => {
|
|
4126
|
+
const out = /* @__PURE__ */ new Set();
|
|
4127
|
+
for (const entry of DEFAULT_METADATA_TYPE_REGISTRY2) {
|
|
4128
|
+
out.add(entry.type);
|
|
4129
|
+
const plural = SINGULAR_TO_PLURAL2[entry.type];
|
|
4130
|
+
if (plural) out.add(plural);
|
|
4131
|
+
}
|
|
4132
|
+
return out;
|
|
4133
|
+
})();
|
|
4134
|
+
_ObjectStackProtocolImplementation.RUNTIME_CREATE_ALLOWED_TYPES = (() => {
|
|
4135
|
+
const out = /* @__PURE__ */ new Set();
|
|
4136
|
+
for (const entry of DEFAULT_METADATA_TYPE_REGISTRY2) {
|
|
4137
|
+
if (!entry.allowRuntimeCreate) continue;
|
|
4138
|
+
out.add(entry.type);
|
|
4139
|
+
const plural = SINGULAR_TO_PLURAL2[entry.type];
|
|
4140
|
+
if (plural) out.add(plural);
|
|
4141
|
+
}
|
|
4142
|
+
return out;
|
|
4143
|
+
})();
|
|
3550
4144
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
3551
4145
|
|
|
3552
4146
|
// src/engine.ts
|