@objectstack/objectql 6.9.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +260 -7
- package/dist/index.d.ts +260 -7
- package/dist/index.js +758 -184
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +750 -176
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.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,58 +1302,59 @@ 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, permissionForm } from "@objectstack/spec/security";
|
|
1159
|
-
import { EmailTemplateSchema, emailTemplateForm, JobSchema } from "@objectstack/spec/system";
|
|
1160
|
-
import { ToolSchema, SkillSchema, AgentSchema, agentForm, toolForm, skillForm } from "@objectstack/spec/ai";
|
|
1161
|
-
import { FlowSchema, WorkflowRuleSchema, ApprovalProcessSchema, flowForm, workflowForm, approvalForm } 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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
|
1357
|
+
var TYPE_TO_FORM = METADATA_FORM_REGISTRY;
|
|
1206
1358
|
var _jsonSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
1207
1359
|
function toJsonSchemaSafe(schema) {
|
|
1208
1360
|
const cached = _jsonSchemaCache.get(schema);
|
|
@@ -1231,9 +1383,17 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1231
1383
|
abstract: { type: "boolean", default: false },
|
|
1232
1384
|
datasource: { type: "string" },
|
|
1233
1385
|
fields: {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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: {
|
|
1237
1397
|
type: "object",
|
|
1238
1398
|
properties: {
|
|
1239
1399
|
name: { type: "string" },
|
|
@@ -1244,7 +1404,7 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1244
1404
|
defaultValue: {},
|
|
1245
1405
|
description: { type: "string" }
|
|
1246
1406
|
},
|
|
1247
|
-
required: ["
|
|
1407
|
+
required: ["type"]
|
|
1248
1408
|
}
|
|
1249
1409
|
},
|
|
1250
1410
|
capabilities: { type: "object", additionalProperties: true }
|
|
@@ -1389,19 +1549,9 @@ var HAND_CRAFTED_SCHEMAS = {
|
|
|
1389
1549
|
additionalProperties: true
|
|
1390
1550
|
}
|
|
1391
1551
|
};
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
switch (singular) {
|
|
1396
|
-
case "view": {
|
|
1397
|
-
const t = item && typeof item === "object" && "type" in item ? String(item.type) : void 0;
|
|
1398
|
-
return t && FORM_VIEW_TYPES.has(t) ? FormViewSchema : ListViewSchema;
|
|
1399
|
-
}
|
|
1400
|
-
case "dashboard":
|
|
1401
|
-
return DashboardSchema;
|
|
1402
|
-
default:
|
|
1403
|
-
return null;
|
|
1404
|
-
}
|
|
1552
|
+
function resolveOverlaySchema(type, _item) {
|
|
1553
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
1554
|
+
return getMetadataTypeSchema2(singular) ?? null;
|
|
1405
1555
|
}
|
|
1406
1556
|
function simpleHash(str) {
|
|
1407
1557
|
let hash = 0;
|
|
@@ -1534,6 +1684,32 @@ function extractPathValues(item, path) {
|
|
|
1534
1684
|
}
|
|
1535
1685
|
return out;
|
|
1536
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
|
+
}
|
|
1537
1713
|
function detectDestructiveObjectChanges(prev, next) {
|
|
1538
1714
|
if (!prev || typeof prev !== "object" || !next || typeof next !== "object") return [];
|
|
1539
1715
|
const prevFields = prev.fields && typeof prev.fields === "object" ? prev.fields : {};
|
|
@@ -1665,6 +1841,20 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1665
1841
|
}
|
|
1666
1842
|
}
|
|
1667
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
|
+
}
|
|
1668
1858
|
} catch {
|
|
1669
1859
|
}
|
|
1670
1860
|
}
|
|
@@ -1788,8 +1978,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1788
1978
|
DEFAULT_METADATA_TYPE_REGISTRY2.map((e) => [e.type, e])
|
|
1789
1979
|
);
|
|
1790
1980
|
const entries = allTypes.map((type) => {
|
|
1791
|
-
const singular =
|
|
1792
|
-
const zodSchema = singular
|
|
1981
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
1982
|
+
const zodSchema = getMetadataTypeSchema2(singular);
|
|
1793
1983
|
const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
|
|
1794
1984
|
const form = TYPE_TO_FORM[singular];
|
|
1795
1985
|
const base = registryByType.get(singular);
|
|
@@ -1830,19 +2020,76 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1830
2020
|
});
|
|
1831
2021
|
return { types: allTypes, entries };
|
|
1832
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
|
+
}
|
|
1833
2080
|
async getMetaItems(request) {
|
|
1834
2081
|
const { packageId } = request;
|
|
1835
2082
|
let items = [];
|
|
1836
2083
|
if (this.environmentId === void 0) {
|
|
1837
2084
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1838
2085
|
if (items.length === 0) {
|
|
1839
|
-
const alt =
|
|
2086
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1840
2087
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1841
2088
|
}
|
|
1842
2089
|
} else {
|
|
1843
2090
|
items = [...this.engine.registry.listItems(request.type, packageId)];
|
|
1844
2091
|
if (items.length === 0) {
|
|
1845
|
-
const alt =
|
|
2092
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1846
2093
|
if (alt) items = [...this.engine.registry.listItems(alt, packageId)];
|
|
1847
2094
|
}
|
|
1848
2095
|
}
|
|
@@ -1857,7 +2104,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1857
2104
|
if (packageId) whereClause._packageId = packageId;
|
|
1858
2105
|
let rs = await this.engine.find("sys_metadata", { where: whereClause });
|
|
1859
2106
|
if (!rs || rs.length === 0) {
|
|
1860
|
-
const alt =
|
|
2107
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1861
2108
|
if (alt) {
|
|
1862
2109
|
const altWhere = { type: alt, state: "active", organization_id: oid };
|
|
1863
2110
|
if (packageId) altWhere._packageId = packageId;
|
|
@@ -1924,28 +2171,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1924
2171
|
}
|
|
1925
2172
|
return {
|
|
1926
2173
|
type: request.type,
|
|
1927
|
-
items
|
|
2174
|
+
items: decorateMetadataItems(request.type, items)
|
|
1928
2175
|
};
|
|
1929
2176
|
}
|
|
1930
2177
|
async getMetaItem(request) {
|
|
1931
2178
|
let item;
|
|
1932
2179
|
const orgId = request.organizationId;
|
|
2180
|
+
const readState = request.state === "draft" ? "draft" : "active";
|
|
1933
2181
|
try {
|
|
1934
2182
|
const findOverlay = async (oid) => {
|
|
1935
2183
|
const where = {
|
|
1936
2184
|
type: request.type,
|
|
1937
2185
|
name: request.name,
|
|
1938
|
-
state:
|
|
2186
|
+
state: readState,
|
|
1939
2187
|
organization_id: oid
|
|
1940
2188
|
};
|
|
1941
2189
|
const rec = await this.engine.findOne("sys_metadata", { where });
|
|
1942
2190
|
if (rec) return rec;
|
|
1943
|
-
const alt =
|
|
2191
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1944
2192
|
if (alt) {
|
|
1945
2193
|
const altWhere = {
|
|
1946
2194
|
type: alt,
|
|
1947
2195
|
name: request.name,
|
|
1948
|
-
state:
|
|
2196
|
+
state: readState,
|
|
1949
2197
|
organization_id: oid
|
|
1950
2198
|
};
|
|
1951
2199
|
return await this.engine.findOne("sys_metadata", { where: altWhere });
|
|
@@ -1958,6 +2206,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1958
2206
|
}
|
|
1959
2207
|
} catch {
|
|
1960
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
|
+
}
|
|
1961
2220
|
if (item === void 0) {
|
|
1962
2221
|
try {
|
|
1963
2222
|
const services = this.getServicesRegistry?.();
|
|
@@ -1967,7 +2226,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1967
2226
|
if (fromService !== void 0 && fromService !== null) {
|
|
1968
2227
|
item = fromService;
|
|
1969
2228
|
} else {
|
|
1970
|
-
const alt =
|
|
2229
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1971
2230
|
if (alt) {
|
|
1972
2231
|
const altFromService = await metadataService.get(alt, request.name);
|
|
1973
2232
|
if (altFromService !== void 0 && altFromService !== null) {
|
|
@@ -1982,14 +2241,14 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1982
2241
|
if (item === void 0) {
|
|
1983
2242
|
item = this.engine.registry.getItem(request.type, request.name);
|
|
1984
2243
|
if (item === void 0) {
|
|
1985
|
-
const alt =
|
|
2244
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
1986
2245
|
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
1987
2246
|
}
|
|
1988
2247
|
}
|
|
1989
2248
|
return {
|
|
1990
2249
|
type: request.type,
|
|
1991
2250
|
name: request.name,
|
|
1992
|
-
item
|
|
2251
|
+
item: decorateMetadataItem(request.type, item)
|
|
1993
2252
|
};
|
|
1994
2253
|
}
|
|
1995
2254
|
/**
|
|
@@ -2015,7 +2274,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2015
2274
|
if (metadataService && typeof metadataService.get === "function") {
|
|
2016
2275
|
let fromService = await metadataService.get(request.type, request.name);
|
|
2017
2276
|
if (fromService === void 0 || fromService === null) {
|
|
2018
|
-
const alt =
|
|
2277
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2019
2278
|
if (alt) fromService = await metadataService.get(alt, request.name);
|
|
2020
2279
|
}
|
|
2021
2280
|
if (fromService !== void 0 && fromService !== null) code = fromService;
|
|
@@ -2025,7 +2284,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2025
2284
|
if (code === null) {
|
|
2026
2285
|
let regItem = this.engine.registry.getItem(request.type, request.name);
|
|
2027
2286
|
if (regItem === void 0) {
|
|
2028
|
-
const alt =
|
|
2287
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2029
2288
|
if (alt) regItem = this.engine.registry.getItem(alt, request.name);
|
|
2030
2289
|
}
|
|
2031
2290
|
if (regItem !== void 0) code = regItem;
|
|
@@ -2042,7 +2301,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2042
2301
|
};
|
|
2043
2302
|
let rec = await this.engine.findOne("sys_metadata", { where });
|
|
2044
2303
|
if (!rec) {
|
|
2045
|
-
const alt =
|
|
2304
|
+
const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
|
|
2046
2305
|
if (alt) {
|
|
2047
2306
|
rec = await this.engine.findOne("sys_metadata", {
|
|
2048
2307
|
where: { ...where, type: alt }
|
|
@@ -2068,13 +2327,15 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2068
2327
|
} catch {
|
|
2069
2328
|
}
|
|
2070
2329
|
const effective = overlay ?? code;
|
|
2330
|
+
const _diagnostics = effective !== null && effective !== void 0 ? computeMetadataDiagnostics(request.type, effective) : void 0;
|
|
2071
2331
|
return {
|
|
2072
2332
|
type: request.type,
|
|
2073
2333
|
name: request.name,
|
|
2074
2334
|
code,
|
|
2075
2335
|
overlay,
|
|
2076
2336
|
overlayScope,
|
|
2077
|
-
effective
|
|
2337
|
+
effective,
|
|
2338
|
+
..._diagnostics ? { _diagnostics } : {}
|
|
2078
2339
|
};
|
|
2079
2340
|
}
|
|
2080
2341
|
async getUiView(request) {
|
|
@@ -2955,7 +3216,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2955
3216
|
for (const tok of raw.split(",")) {
|
|
2956
3217
|
const t = tok.trim();
|
|
2957
3218
|
if (!t) continue;
|
|
2958
|
-
const singular =
|
|
3219
|
+
const singular = PLURAL_TO_SINGULAR3[t] ?? t;
|
|
2959
3220
|
set.add(singular);
|
|
2960
3221
|
const plural = SINGULAR_TO_PLURAL2[singular];
|
|
2961
3222
|
if (plural) set.add(plural);
|
|
@@ -2969,13 +3230,49 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2969
3230
|
}
|
|
2970
3231
|
/** Normalize plural→singular before consulting the allow-list. */
|
|
2971
3232
|
static isOverlayAllowed(type) {
|
|
2972
|
-
const singular =
|
|
3233
|
+
const singular = PLURAL_TO_SINGULAR3[type] ?? type;
|
|
2973
3234
|
if (this.OVERLAY_ALLOWED_TYPES.has(singular) || this.OVERLAY_ALLOWED_TYPES.has(type)) {
|
|
2974
3235
|
return true;
|
|
2975
3236
|
}
|
|
2976
3237
|
const env = this.envWritableTypes();
|
|
2977
3238
|
return env.has(singular) || env.has(type);
|
|
2978
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
|
+
}
|
|
2979
3276
|
/**
|
|
2980
3277
|
* Mirror an object-type overlay write into the in-memory engine
|
|
2981
3278
|
* registry so subsequent CRUD finds the new schema. Idempotent and
|
|
@@ -3001,16 +3298,29 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3001
3298
|
if (!request.item) {
|
|
3002
3299
|
throw new Error("Item data is required");
|
|
3003
3300
|
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
const
|
|
3007
|
-
|
|
3008
|
-
);
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
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
|
+
}
|
|
3012
3322
|
}
|
|
3013
|
-
const singularType =
|
|
3323
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3014
3324
|
if (!request.force && (singularType === "object" || singularType === "field")) {
|
|
3015
3325
|
try {
|
|
3016
3326
|
const existing = await this.getMetaItem({
|
|
@@ -3058,8 +3368,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3058
3368
|
}
|
|
3059
3369
|
}
|
|
3060
3370
|
await this.ensureOverlayIndex();
|
|
3061
|
-
const singularTypeForRepo =
|
|
3062
|
-
|
|
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";
|
|
3063
3378
|
const orgId = request.organizationId ?? null;
|
|
3064
3379
|
const repo = this.getOverlayRepo(orgId);
|
|
3065
3380
|
const ref = {
|
|
@@ -3071,21 +3386,26 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3071
3386
|
if (request.parentVersion !== void 0) {
|
|
3072
3387
|
parentVersion = request.parentVersion;
|
|
3073
3388
|
} else {
|
|
3074
|
-
const current = await repo.get(ref);
|
|
3389
|
+
const current = await repo.get(ref, { state: mode === "draft" ? "draft" : "active" });
|
|
3075
3390
|
parentVersion = current?.hash ?? null;
|
|
3076
3391
|
}
|
|
3077
3392
|
try {
|
|
3078
3393
|
const result = await repo.put(ref, request.item, {
|
|
3079
3394
|
parentVersion,
|
|
3080
3395
|
actor: request.actor ?? "system",
|
|
3081
|
-
source: "protocol.saveMetaItem"
|
|
3396
|
+
source: "protocol.saveMetaItem",
|
|
3397
|
+
intent,
|
|
3398
|
+
state: mode === "draft" ? "draft" : "active"
|
|
3082
3399
|
});
|
|
3083
|
-
|
|
3400
|
+
if (mode === "publish") {
|
|
3401
|
+
this.applyObjectRegistryMutation(request);
|
|
3402
|
+
}
|
|
3084
3403
|
return {
|
|
3085
3404
|
success: true,
|
|
3086
3405
|
version: result.version,
|
|
3087
3406
|
seq: result.seq,
|
|
3088
|
-
|
|
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}]`
|
|
3089
3409
|
};
|
|
3090
3410
|
} catch (err) {
|
|
3091
3411
|
if (err instanceof ConflictError2) {
|
|
@@ -3169,8 +3489,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3169
3489
|
* "no history" uniformly.
|
|
3170
3490
|
*/
|
|
3171
3491
|
async historyMetaItem(request) {
|
|
3172
|
-
const singularType =
|
|
3173
|
-
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
|
|
3492
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3493
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3174
3494
|
return { events: [] };
|
|
3175
3495
|
}
|
|
3176
3496
|
const orgId = request.organizationId ?? null;
|
|
@@ -3188,22 +3508,238 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3188
3508
|
return { events };
|
|
3189
3509
|
}
|
|
3190
3510
|
/**
|
|
3191
|
-
*
|
|
3192
|
-
*
|
|
3193
|
-
*
|
|
3194
|
-
* 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.
|
|
3195
3514
|
*/
|
|
3196
|
-
async
|
|
3197
|
-
|
|
3515
|
+
async publishMetaItem(request) {
|
|
3516
|
+
const singularType = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3517
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !_ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) {
|
|
3198
3518
|
const err = new Error(
|
|
3199
|
-
`[not_overridable] Metadata type '${request.type}'
|
|
3519
|
+
`[not_overridable] Metadata type '${request.type}' is not draftable \u2014 no overlay/runtime-create permission.`
|
|
3200
3520
|
);
|
|
3201
3521
|
err.code = "not_overridable";
|
|
3202
3522
|
err.status = 403;
|
|
3203
3523
|
throw err;
|
|
3204
3524
|
}
|
|
3205
|
-
|
|
3206
|
-
const
|
|
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)) {
|
|
3585
|
+
const err = new Error(
|
|
3586
|
+
`[not_overridable] Metadata type '${request.type}' is not revertable \u2014 no overlay/runtime-create permission.`
|
|
3587
|
+
);
|
|
3588
|
+
err.code = "not_overridable";
|
|
3589
|
+
err.status = 403;
|
|
3590
|
+
throw err;
|
|
3591
|
+
}
|
|
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;
|
|
3207
3743
|
if (useRepoPath) {
|
|
3208
3744
|
const orgId = request.organizationId ?? null;
|
|
3209
3745
|
const repo = this.getOverlayRepo(orgId);
|
|
@@ -3213,19 +3749,22 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3213
3749
|
org: orgId ?? "env"
|
|
3214
3750
|
};
|
|
3215
3751
|
try {
|
|
3216
|
-
const
|
|
3752
|
+
const targetState = request.state === "draft" ? "draft" : "active";
|
|
3753
|
+
const current = await repo.get(ref, { state: targetState });
|
|
3217
3754
|
if (!current) {
|
|
3218
3755
|
return {
|
|
3219
3756
|
success: true,
|
|
3220
3757
|
reset: false,
|
|
3221
|
-
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.`
|
|
3222
3759
|
};
|
|
3223
3760
|
}
|
|
3224
3761
|
const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
|
|
3225
3762
|
const result = await repo.delete(ref, {
|
|
3226
3763
|
parentVersion,
|
|
3227
3764
|
actor: request.actor ?? "system",
|
|
3228
|
-
source: "protocol.deleteMetaItem"
|
|
3765
|
+
source: "protocol.deleteMetaItem",
|
|
3766
|
+
intent: this.isArtifactBacked(singularTypeForRepo, request.name) ? "override-artifact" : "runtime-only",
|
|
3767
|
+
state: targetState
|
|
3229
3768
|
});
|
|
3230
3769
|
if (this.environmentId === void 0) {
|
|
3231
3770
|
try {
|
|
@@ -3244,7 +3783,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3244
3783
|
success: true,
|
|
3245
3784
|
reset: true,
|
|
3246
3785
|
seq: result.seq,
|
|
3247
|
-
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}]`
|
|
3248
3787
|
};
|
|
3249
3788
|
} catch (err) {
|
|
3250
3789
|
if (err instanceof ConflictError2) {
|
|
@@ -3322,7 +3861,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3322
3861
|
for (const record of records) {
|
|
3323
3862
|
try {
|
|
3324
3863
|
const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
|
|
3325
|
-
const normalizedType =
|
|
3864
|
+
const normalizedType = PLURAL_TO_SINGULAR3[record.type] ?? record.type;
|
|
3326
3865
|
if (normalizedType === "object") {
|
|
3327
3866
|
this.engine.registry.registerObject(data, record.packageId || "sys_metadata");
|
|
3328
3867
|
} else {
|
|
@@ -3356,7 +3895,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
3356
3895
|
* — the engine never throws.
|
|
3357
3896
|
*/
|
|
3358
3897
|
async findReferencesToMeta(request) {
|
|
3359
|
-
const singularTarget =
|
|
3898
|
+
const singularTarget = PLURAL_TO_SINGULAR3[request.type] ?? request.type;
|
|
3360
3899
|
const targetName = request.name;
|
|
3361
3900
|
const matchers = REFERENCE_PATHS[singularTarget];
|
|
3362
3901
|
if (!matchers || matchers.length === 0) {
|
|
@@ -3567,6 +4106,41 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
|
3567
4106
|
* {@link ObjectStackProtocolImplementation.resetEnvWritableCache}.
|
|
3568
4107
|
*/
|
|
3569
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
|
+
})();
|
|
3570
4144
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
3571
4145
|
|
|
3572
4146
|
// src/engine.ts
|