@objectstack/objectql 4.1.1 → 5.0.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 +237 -1
- package/dist/index.d.ts +237 -1
- package/dist/index.js +833 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +830 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ __export(index_exports, {
|
|
|
31
31
|
RESERVED_NAMESPACES: () => RESERVED_NAMESPACES,
|
|
32
32
|
SchemaRegistry: () => SchemaRegistry,
|
|
33
33
|
ScopedContext: () => ScopedContext,
|
|
34
|
+
SysMetadataRepository: () => SysMetadataRepository,
|
|
34
35
|
ValidationError: () => ValidationError,
|
|
35
36
|
applyInMemoryAggregation: () => applyInMemoryAggregation,
|
|
36
37
|
applySystemFields: () => applySystemFields,
|
|
@@ -628,6 +629,34 @@ var SchemaRegistry = class {
|
|
|
628
629
|
// ==========================================
|
|
629
630
|
// Reset (for testing)
|
|
630
631
|
// ==========================================
|
|
632
|
+
/**
|
|
633
|
+
* Invalidate the merged-schema cache for a single FQN (or short name).
|
|
634
|
+
*
|
|
635
|
+
* Call this from event-driven consumers (ADR-0008 M0 PR-7) when an
|
|
636
|
+
* upstream metadata change makes the cached merged definition stale.
|
|
637
|
+
* The contributor list is preserved — only the cached merge result is
|
|
638
|
+
* dropped, so the next `resolveObject(fqn)` recomputes from scratch.
|
|
639
|
+
*
|
|
640
|
+
* Accepts either an FQN (`acme__contact`) or a bare short name
|
|
641
|
+
* (`contact`); for the latter, all entries whose suffix matches the
|
|
642
|
+
* name are invalidated.
|
|
643
|
+
*/
|
|
644
|
+
invalidate(fqnOrName) {
|
|
645
|
+
if (this.mergedObjectCache.has(fqnOrName)) {
|
|
646
|
+
this.mergedObjectCache.delete(fqnOrName);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const suffix = `__${fqnOrName}`;
|
|
650
|
+
for (const fqn of Array.from(this.mergedObjectCache.keys())) {
|
|
651
|
+
if (fqn === fqnOrName || fqn.endsWith(suffix)) {
|
|
652
|
+
this.mergedObjectCache.delete(fqn);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
/** Drop every entry from the merged-schema cache. */
|
|
657
|
+
invalidateAll() {
|
|
658
|
+
this.mergedObjectCache.clear();
|
|
659
|
+
}
|
|
631
660
|
/**
|
|
632
661
|
* Clear all registry state. Use only for testing.
|
|
633
662
|
*/
|
|
@@ -640,11 +669,487 @@ var SchemaRegistry = class {
|
|
|
640
669
|
}
|
|
641
670
|
};
|
|
642
671
|
|
|
672
|
+
// src/sys-metadata-repository.ts
|
|
673
|
+
var import_metadata_core = require("@objectstack/metadata-core");
|
|
674
|
+
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
675
|
+
var OVERLAY_ALLOWED_TYPES = new Set(
|
|
676
|
+
import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowOrgOverride).map((e) => e.type)
|
|
677
|
+
);
|
|
678
|
+
var SysMetadataRepository = class {
|
|
679
|
+
constructor(opts) {
|
|
680
|
+
/**
|
|
681
|
+
* Local seq counter for in-memory watch() event broadcasts. Mirrors
|
|
682
|
+
* the durable `event_seq` we write into `sys_metadata_history` on
|
|
683
|
+
* each successful put/delete — assigned AFTER the transaction commits
|
|
684
|
+
* so we never broadcast events that got rolled back.
|
|
685
|
+
*/
|
|
686
|
+
this.seqCounter = 0;
|
|
687
|
+
this.watchers = /* @__PURE__ */ new Set();
|
|
688
|
+
this.closed = false;
|
|
689
|
+
/** Table name for the durable event log. */
|
|
690
|
+
this.historyTable = "sys_metadata_history";
|
|
691
|
+
this.engine = opts.engine;
|
|
692
|
+
this.organizationId = opts.organizationId ?? null;
|
|
693
|
+
this.orgLabel = opts.orgLabel ?? (opts.organizationId ?? "system");
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Run `cb` inside `engine.transaction(...)` if the engine supports it,
|
|
697
|
+
* otherwise fall through to a direct call. Matches the real
|
|
698
|
+
* `ObjectQL.transaction` semantics — in-memory drivers (and our test
|
|
699
|
+
* fakes) get no rollback, which is acceptable because production
|
|
700
|
+
* always runs on a SQL driver with real ACID.
|
|
701
|
+
*/
|
|
702
|
+
async withTxn(cb) {
|
|
703
|
+
if (typeof this.engine.transaction === "function") {
|
|
704
|
+
return this.engine.transaction(cb);
|
|
705
|
+
}
|
|
706
|
+
return cb(void 0);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Read the current overlay row. Returns null if no row exists —
|
|
710
|
+
* callers (e.g. LayeredRepository) fall through to lower layers.
|
|
711
|
+
*/
|
|
712
|
+
async get(ref) {
|
|
713
|
+
this.assertOpen();
|
|
714
|
+
const row = await this.engine.findOne("sys_metadata", {
|
|
715
|
+
where: this.whereFor(ref)
|
|
716
|
+
});
|
|
717
|
+
if (!row) return null;
|
|
718
|
+
return this.rowToItem(ref, row);
|
|
719
|
+
}
|
|
720
|
+
async put(ref, spec, opts) {
|
|
721
|
+
this.assertOpen();
|
|
722
|
+
this.assertAllowed(ref.type);
|
|
723
|
+
const body = spec ?? {};
|
|
724
|
+
const hash = (0, import_metadata_core.hashSpec)(body);
|
|
725
|
+
const result = await this.withTxn(async (ctx) => {
|
|
726
|
+
const existing = await this.engine.findOne("sys_metadata", {
|
|
727
|
+
where: this.whereFor(ref),
|
|
728
|
+
context: ctx
|
|
729
|
+
});
|
|
730
|
+
const existingHash = existing?.checksum ?? null;
|
|
731
|
+
if (opts.parentVersion !== existingHash) {
|
|
732
|
+
throw new import_metadata_core.ConflictError(this.fullRef(ref), opts.parentVersion, existingHash);
|
|
733
|
+
}
|
|
734
|
+
if (existing && existingHash === hash) {
|
|
735
|
+
const item2 = this.rowToItem(ref, existing);
|
|
736
|
+
return { skipped: true, version: hash, seq: item2.seq, item: item2 };
|
|
737
|
+
}
|
|
738
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
739
|
+
const op = existing ? "update" : "create";
|
|
740
|
+
const version = await this.nextItemVersion(ref, ctx);
|
|
741
|
+
const eventSeq = await this.nextEventSeq(ctx);
|
|
742
|
+
const parentRowData = {
|
|
743
|
+
type: ref.type,
|
|
744
|
+
name: ref.name,
|
|
745
|
+
organization_id: this.organizationId,
|
|
746
|
+
metadata: JSON.stringify(body),
|
|
747
|
+
checksum: hash,
|
|
748
|
+
state: "active",
|
|
749
|
+
version,
|
|
750
|
+
updated_at: now
|
|
751
|
+
};
|
|
752
|
+
let parentId;
|
|
753
|
+
if (existing) {
|
|
754
|
+
const existingId = existing.id;
|
|
755
|
+
if (existingId === void 0) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
`SysMetadataRepository.put: existing row for ${ref.type}/${ref.name} has no id column`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
parentId = existingId;
|
|
761
|
+
await this.engine.update("sys_metadata", parentRowData, {
|
|
762
|
+
where: { id: existingId },
|
|
763
|
+
context: ctx
|
|
764
|
+
});
|
|
765
|
+
} else {
|
|
766
|
+
parentRowData.created_at = now;
|
|
767
|
+
const inserted = await this.engine.insert("sys_metadata", parentRowData, { context: ctx });
|
|
768
|
+
parentId = inserted.id;
|
|
769
|
+
}
|
|
770
|
+
await this.engine.insert(
|
|
771
|
+
this.historyTable,
|
|
772
|
+
{
|
|
773
|
+
id: this.uuid(),
|
|
774
|
+
event_seq: eventSeq,
|
|
775
|
+
metadata_id: parentId,
|
|
776
|
+
type: ref.type,
|
|
777
|
+
name: ref.name,
|
|
778
|
+
version,
|
|
779
|
+
operation_type: op,
|
|
780
|
+
metadata: JSON.stringify(body),
|
|
781
|
+
checksum: hash,
|
|
782
|
+
previous_checksum: existingHash,
|
|
783
|
+
change_note: opts.message,
|
|
784
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
785
|
+
organization_id: this.organizationId,
|
|
786
|
+
recorded_by: opts.actor,
|
|
787
|
+
recorded_at: now
|
|
788
|
+
},
|
|
789
|
+
{ context: ctx }
|
|
790
|
+
);
|
|
791
|
+
const item = {
|
|
792
|
+
ref: this.fullRef(ref),
|
|
793
|
+
body,
|
|
794
|
+
hash,
|
|
795
|
+
parentHash: existingHash,
|
|
796
|
+
authoredBy: opts.actor,
|
|
797
|
+
authoredAt: now,
|
|
798
|
+
message: opts.message,
|
|
799
|
+
seq: eventSeq
|
|
800
|
+
};
|
|
801
|
+
return {
|
|
802
|
+
skipped: false,
|
|
803
|
+
version: hash,
|
|
804
|
+
seq: eventSeq,
|
|
805
|
+
item,
|
|
806
|
+
op,
|
|
807
|
+
existingHash,
|
|
808
|
+
now,
|
|
809
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
810
|
+
message: opts.message,
|
|
811
|
+
actor: opts.actor
|
|
812
|
+
};
|
|
813
|
+
});
|
|
814
|
+
if (result.skipped) {
|
|
815
|
+
return { version: result.version, seq: result.seq, item: result.item };
|
|
816
|
+
}
|
|
817
|
+
this.seqCounter = result.seq;
|
|
818
|
+
this.broadcast({
|
|
819
|
+
seq: result.seq,
|
|
820
|
+
op: result.op,
|
|
821
|
+
ref: this.fullRef(ref),
|
|
822
|
+
hash: result.version,
|
|
823
|
+
parentHash: result.existingHash,
|
|
824
|
+
actor: result.actor,
|
|
825
|
+
message: result.message,
|
|
826
|
+
ts: result.now,
|
|
827
|
+
source: result.source
|
|
828
|
+
});
|
|
829
|
+
return { version: result.version, seq: result.seq, item: result.item };
|
|
830
|
+
}
|
|
831
|
+
async delete(ref, opts) {
|
|
832
|
+
this.assertOpen();
|
|
833
|
+
this.assertAllowed(ref.type);
|
|
834
|
+
const result = await this.withTxn(async (ctx) => {
|
|
835
|
+
const existing = await this.engine.findOne("sys_metadata", {
|
|
836
|
+
where: this.whereFor(ref),
|
|
837
|
+
context: ctx
|
|
838
|
+
});
|
|
839
|
+
if (!existing) {
|
|
840
|
+
throw new import_metadata_core.ConflictError(this.fullRef(ref), opts.parentVersion, null);
|
|
841
|
+
}
|
|
842
|
+
const existingHash = existing.checksum ?? null;
|
|
843
|
+
if (opts.parentVersion !== existingHash) {
|
|
844
|
+
throw new import_metadata_core.ConflictError(this.fullRef(ref), opts.parentVersion, existingHash);
|
|
845
|
+
}
|
|
846
|
+
const existingId = existing.id;
|
|
847
|
+
if (existingId === void 0) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`SysMetadataRepository.delete: existing row for ${ref.type}/${ref.name} has no id column`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
853
|
+
const version = await this.nextItemVersion(ref, ctx);
|
|
854
|
+
const eventSeq = await this.nextEventSeq(ctx);
|
|
855
|
+
await this.engine.delete("sys_metadata", {
|
|
856
|
+
where: { id: existingId },
|
|
857
|
+
context: ctx
|
|
858
|
+
});
|
|
859
|
+
await this.engine.insert(
|
|
860
|
+
this.historyTable,
|
|
861
|
+
{
|
|
862
|
+
id: this.uuid(),
|
|
863
|
+
event_seq: eventSeq,
|
|
864
|
+
metadata_id: existingId,
|
|
865
|
+
type: ref.type,
|
|
866
|
+
name: ref.name,
|
|
867
|
+
version,
|
|
868
|
+
operation_type: "delete",
|
|
869
|
+
metadata: null,
|
|
870
|
+
checksum: null,
|
|
871
|
+
previous_checksum: existingHash,
|
|
872
|
+
change_note: opts.message,
|
|
873
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
874
|
+
organization_id: this.organizationId,
|
|
875
|
+
recorded_by: opts.actor,
|
|
876
|
+
recorded_at: now
|
|
877
|
+
},
|
|
878
|
+
{ context: ctx }
|
|
879
|
+
);
|
|
880
|
+
return {
|
|
881
|
+
eventSeq,
|
|
882
|
+
existingHash,
|
|
883
|
+
now,
|
|
884
|
+
source: opts.source ?? "sys-metadata-repo",
|
|
885
|
+
message: opts.message,
|
|
886
|
+
actor: opts.actor
|
|
887
|
+
};
|
|
888
|
+
});
|
|
889
|
+
this.seqCounter = result.eventSeq;
|
|
890
|
+
this.broadcast({
|
|
891
|
+
seq: result.eventSeq,
|
|
892
|
+
op: "delete",
|
|
893
|
+
ref: this.fullRef(ref),
|
|
894
|
+
hash: null,
|
|
895
|
+
parentHash: result.existingHash,
|
|
896
|
+
actor: result.actor,
|
|
897
|
+
message: result.message,
|
|
898
|
+
ts: result.now,
|
|
899
|
+
source: result.source
|
|
900
|
+
});
|
|
901
|
+
return { seq: result.eventSeq };
|
|
902
|
+
}
|
|
903
|
+
async *list(filter) {
|
|
904
|
+
this.assertOpen();
|
|
905
|
+
const where = {
|
|
906
|
+
organization_id: this.organizationId,
|
|
907
|
+
state: "active"
|
|
908
|
+
};
|
|
909
|
+
if (filter.type) where.type = filter.type;
|
|
910
|
+
const rows = await this.engine.find("sys_metadata", {
|
|
911
|
+
where,
|
|
912
|
+
limit: filter.limit
|
|
913
|
+
});
|
|
914
|
+
for (const row of rows) {
|
|
915
|
+
if (filter.nameContains && !String(row.name).includes(filter.nameContains)) continue;
|
|
916
|
+
const item = this.rowToItem(
|
|
917
|
+
{ ...this.fullRef({ type: row.type, name: row.name }) },
|
|
918
|
+
row
|
|
919
|
+
);
|
|
920
|
+
const { body, ...header } = item;
|
|
921
|
+
yield header;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Yield every history event for `(org, type?, name?)` from the
|
|
926
|
+
* durable log, ordered by per-(type,name) `version` ascending. When
|
|
927
|
+
* `filter.type`/`filter.name` are unset the consumer gets the full
|
|
928
|
+
* org-scoped event stream — still ordered by version within each
|
|
929
|
+
* (type,name) bucket, then by `recorded_at` across buckets (we sort
|
|
930
|
+
* client-side because the test engine doesn't honor `orderBy`).
|
|
931
|
+
*/
|
|
932
|
+
async *history(ref, opts) {
|
|
933
|
+
this.assertOpen();
|
|
934
|
+
const full = this.fullRef(ref);
|
|
935
|
+
const where = {
|
|
936
|
+
organization_id: this.organizationId,
|
|
937
|
+
type: full.type,
|
|
938
|
+
name: full.name
|
|
939
|
+
};
|
|
940
|
+
const rows = await this.engine.find(this.historyTable, { where });
|
|
941
|
+
rows.sort((a, b) => {
|
|
942
|
+
const va = typeof a.event_seq === "number" ? a.event_seq : 0;
|
|
943
|
+
const vb = typeof b.event_seq === "number" ? b.event_seq : 0;
|
|
944
|
+
return va - vb;
|
|
945
|
+
});
|
|
946
|
+
let yielded = 0;
|
|
947
|
+
for (const row of rows) {
|
|
948
|
+
if (opts?.sinceSeq !== void 0 && (row.event_seq ?? 0) <= opts.sinceSeq) continue;
|
|
949
|
+
if (opts?.limit !== void 0 && yielded >= opts.limit) break;
|
|
950
|
+
yielded++;
|
|
951
|
+
yield {
|
|
952
|
+
seq: row.event_seq ?? 0,
|
|
953
|
+
op: row.operation_type ?? "update",
|
|
954
|
+
ref: full,
|
|
955
|
+
hash: row.checksum ?? null,
|
|
956
|
+
parentHash: row.previous_checksum ?? null,
|
|
957
|
+
actor: row.recorded_by ?? "unknown",
|
|
958
|
+
message: row.change_note ?? void 0,
|
|
959
|
+
ts: row.recorded_at ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
960
|
+
source: row.source ?? "sys-metadata-repo"
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Live event stream. Fires for every successful put/delete on THIS
|
|
966
|
+
* instance — cross-replica fan-out is M1. Manual AsyncIterator (not
|
|
967
|
+
* an async generator) so we can deterministically tear down via
|
|
968
|
+
* `iter.return()`, matching the pattern used by InMemoryRepository.
|
|
969
|
+
*/
|
|
970
|
+
watch(filter, since) {
|
|
971
|
+
const self = this;
|
|
972
|
+
return {
|
|
973
|
+
[Symbol.asyncIterator]: () => {
|
|
974
|
+
const queue = [];
|
|
975
|
+
let pendingResolve = null;
|
|
976
|
+
let stopped = false;
|
|
977
|
+
const dispatch = (evt) => {
|
|
978
|
+
if (stopped) return;
|
|
979
|
+
if (!self.matchesFilter(evt, filter)) return;
|
|
980
|
+
if (since !== void 0 && evt.seq <= since) return;
|
|
981
|
+
if (pendingResolve) {
|
|
982
|
+
const r = pendingResolve;
|
|
983
|
+
pendingResolve = null;
|
|
984
|
+
r({ value: evt, done: false });
|
|
985
|
+
} else {
|
|
986
|
+
queue.push(evt);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
self.watchers.add(dispatch);
|
|
990
|
+
return {
|
|
991
|
+
next() {
|
|
992
|
+
if (stopped) return Promise.resolve({ value: void 0, done: true });
|
|
993
|
+
const buffered = queue.shift();
|
|
994
|
+
if (buffered) return Promise.resolve({ value: buffered, done: false });
|
|
995
|
+
return new Promise((resolve) => {
|
|
996
|
+
pendingResolve = resolve;
|
|
997
|
+
});
|
|
998
|
+
},
|
|
999
|
+
return() {
|
|
1000
|
+
stopped = true;
|
|
1001
|
+
self.watchers.delete(dispatch);
|
|
1002
|
+
if (pendingResolve) {
|
|
1003
|
+
const r = pendingResolve;
|
|
1004
|
+
pendingResolve = null;
|
|
1005
|
+
r({ value: void 0, done: true });
|
|
1006
|
+
}
|
|
1007
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
/** Shut down all watch iterators. */
|
|
1014
|
+
close() {
|
|
1015
|
+
this.closed = true;
|
|
1016
|
+
const snapshot = Array.from(this.watchers);
|
|
1017
|
+
for (const w of snapshot) {
|
|
1018
|
+
try {
|
|
1019
|
+
w({
|
|
1020
|
+
seq: -1,
|
|
1021
|
+
op: "delete",
|
|
1022
|
+
ref: { org: "", type: "view", name: "_close" },
|
|
1023
|
+
hash: null,
|
|
1024
|
+
parentHash: null,
|
|
1025
|
+
actor: "system",
|
|
1026
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1027
|
+
source: "sys-metadata-repo-close"
|
|
1028
|
+
});
|
|
1029
|
+
} catch {
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
this.watchers.clear();
|
|
1033
|
+
}
|
|
1034
|
+
// ── helpers ─────────────────────────────────────────────────────────
|
|
1035
|
+
assertOpen() {
|
|
1036
|
+
if (this.closed) throw new Error("SysMetadataRepository is closed");
|
|
1037
|
+
}
|
|
1038
|
+
assertAllowed(type) {
|
|
1039
|
+
if (!OVERLAY_ALLOWED_TYPES.has(type)) {
|
|
1040
|
+
const err = new Error(
|
|
1041
|
+
`[not_overridable] '${type}' is not allowOrgOverride in the registry. Allowed: ${Array.from(OVERLAY_ALLOWED_TYPES).join(", ")}.`
|
|
1042
|
+
);
|
|
1043
|
+
err.code = "not_overridable";
|
|
1044
|
+
err.status = 403;
|
|
1045
|
+
throw err;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
whereFor(ref) {
|
|
1049
|
+
return {
|
|
1050
|
+
type: ref.type,
|
|
1051
|
+
name: ref.name,
|
|
1052
|
+
organization_id: this.organizationId,
|
|
1053
|
+
state: "active"
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
fullRef(ref) {
|
|
1057
|
+
return {
|
|
1058
|
+
org: this.orgLabel,
|
|
1059
|
+
type: ref.type,
|
|
1060
|
+
name: ref.name
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
rowToItem(ref, row) {
|
|
1064
|
+
const body = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata ?? {};
|
|
1065
|
+
const hash = row.checksum ?? (0, import_metadata_core.hashSpec)(body);
|
|
1066
|
+
return {
|
|
1067
|
+
ref: this.fullRef(ref),
|
|
1068
|
+
body,
|
|
1069
|
+
hash,
|
|
1070
|
+
parentHash: null,
|
|
1071
|
+
authoredBy: row.updated_by ?? row.created_by ?? "unknown",
|
|
1072
|
+
authoredAt: row.updated_at ?? row.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1073
|
+
message: void 0,
|
|
1074
|
+
seq: this.seqCounter
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
broadcast(evt) {
|
|
1078
|
+
for (const w of Array.from(this.watchers)) {
|
|
1079
|
+
try {
|
|
1080
|
+
w(evt);
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
matchesFilter(evt, filter) {
|
|
1086
|
+
if (filter.type && evt.ref.type !== filter.type) return false;
|
|
1087
|
+
if (filter.name && evt.ref.name !== filter.name) return false;
|
|
1088
|
+
if (filter.org && evt.ref.org !== filter.org) return false;
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Per-org monotonic event sequence. Reads `MAX(event_seq) + 1` from
|
|
1093
|
+
* `sys_metadata_history` scoped by `organization_id`. MUST be called
|
|
1094
|
+
* inside a transaction (the only caller is the put/delete txn body) —
|
|
1095
|
+
* concurrent writers in the same org race otherwise.
|
|
1096
|
+
*/
|
|
1097
|
+
async nextEventSeq(ctx) {
|
|
1098
|
+
try {
|
|
1099
|
+
const rows = await this.engine.find(this.historyTable, {
|
|
1100
|
+
where: { organization_id: this.organizationId },
|
|
1101
|
+
context: ctx
|
|
1102
|
+
});
|
|
1103
|
+
let max = 0;
|
|
1104
|
+
for (const row of rows) {
|
|
1105
|
+
const v = typeof row.event_seq === "number" ? row.event_seq : 0;
|
|
1106
|
+
if (v > max) max = v;
|
|
1107
|
+
}
|
|
1108
|
+
return max + 1;
|
|
1109
|
+
} catch {
|
|
1110
|
+
return 1;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Per-(org,type,name) lineage counter. Reads from history (not from
|
|
1115
|
+
* `sys_metadata.version`) so delete + recreate continues incrementing
|
|
1116
|
+
* instead of restarting at 1.
|
|
1117
|
+
*/
|
|
1118
|
+
async nextItemVersion(ref, ctx) {
|
|
1119
|
+
try {
|
|
1120
|
+
const rows = await this.engine.find(this.historyTable, {
|
|
1121
|
+
where: {
|
|
1122
|
+
organization_id: this.organizationId,
|
|
1123
|
+
type: ref.type,
|
|
1124
|
+
name: ref.name
|
|
1125
|
+
},
|
|
1126
|
+
context: ctx
|
|
1127
|
+
});
|
|
1128
|
+
let max = 0;
|
|
1129
|
+
for (const row of rows) {
|
|
1130
|
+
const v = typeof row.version === "number" ? row.version : 0;
|
|
1131
|
+
if (v > max) max = v;
|
|
1132
|
+
}
|
|
1133
|
+
return max + 1;
|
|
1134
|
+
} catch {
|
|
1135
|
+
return 1;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/** Lightweight UUID-ish id for history rows; sufficient for an audit log. */
|
|
1139
|
+
uuid() {
|
|
1140
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
1141
|
+
return globalThis.crypto.randomUUID();
|
|
1142
|
+
}
|
|
1143
|
+
return `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
|
|
643
1147
|
// src/protocol.ts
|
|
1148
|
+
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
644
1149
|
var import_data2 = require("@objectstack/spec/data");
|
|
645
1150
|
var import_shared = require("@objectstack/spec/shared");
|
|
646
1151
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
647
|
-
var
|
|
1152
|
+
var import_kernel3 = require("@objectstack/spec/kernel");
|
|
648
1153
|
var FORM_VIEW_TYPES = /* @__PURE__ */ new Set(["simple", "tabbed", "wizard", "split", "drawer", "modal"]);
|
|
649
1154
|
function resolveOverlaySchema(type, item) {
|
|
650
1155
|
const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
|
|
@@ -668,6 +1173,25 @@ function simpleHash(str) {
|
|
|
668
1173
|
}
|
|
669
1174
|
return Math.abs(hash).toString(16);
|
|
670
1175
|
}
|
|
1176
|
+
var ConcurrentUpdateError = class extends Error {
|
|
1177
|
+
constructor(opts) {
|
|
1178
|
+
super(opts.message ?? "Record was modified by another user");
|
|
1179
|
+
this.code = "CONCURRENT_UPDATE";
|
|
1180
|
+
this.status = 409;
|
|
1181
|
+
this.name = "ConcurrentUpdateError";
|
|
1182
|
+
this.currentVersion = opts.currentVersion;
|
|
1183
|
+
this.currentRecord = opts.currentRecord;
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
function normaliseVersionToken(v) {
|
|
1187
|
+
if (v === null || v === void 0) return null;
|
|
1188
|
+
const s = String(v).trim();
|
|
1189
|
+
if (!s) return null;
|
|
1190
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
1191
|
+
return s.slice(1, -1);
|
|
1192
|
+
}
|
|
1193
|
+
return s;
|
|
1194
|
+
}
|
|
671
1195
|
var SERVICE_CONFIG = {
|
|
672
1196
|
auth: { route: "/api/v1/auth", plugin: "plugin-auth" },
|
|
673
1197
|
automation: { route: "/api/v1/automation", plugin: "plugin-automation" },
|
|
@@ -687,6 +1211,13 @@ var SERVICE_CONFIG = {
|
|
|
687
1211
|
};
|
|
688
1212
|
var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementation {
|
|
689
1213
|
constructor(engine, getServicesRegistry, getFeedService, projectId) {
|
|
1214
|
+
/**
|
|
1215
|
+
* Lazily-instantiated SysMetadataRepository per organization. Keyed by
|
|
1216
|
+
* `${organizationId ?? '__env__'}`. Repositories are stateful — they
|
|
1217
|
+
* carry the per-org `seqCounter` and watch subscribers — so we cache
|
|
1218
|
+
* them rather than constructing one per call.
|
|
1219
|
+
*/
|
|
1220
|
+
this.overlayRepos = /* @__PURE__ */ new Map();
|
|
690
1221
|
/**
|
|
691
1222
|
* One-time guard for ensuring the overlay-uniqueness UNIQUE INDEX exists
|
|
692
1223
|
* on `sys_metadata`. ADR-0005: scopes overlays by
|
|
@@ -702,6 +1233,24 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
702
1233
|
this.getFeedService = getFeedService;
|
|
703
1234
|
this.projectId = projectId;
|
|
704
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Lazily obtain a SysMetadataRepository for the given organization.
|
|
1238
|
+
* Env-wide overlays (organizationId == null) share a singleton under
|
|
1239
|
+
* the `__env__` key.
|
|
1240
|
+
*/
|
|
1241
|
+
getOverlayRepo(organizationId) {
|
|
1242
|
+
const key = organizationId ?? "__env__";
|
|
1243
|
+
let repo = this.overlayRepos.get(key);
|
|
1244
|
+
if (!repo) {
|
|
1245
|
+
repo = new SysMetadataRepository({
|
|
1246
|
+
engine: this.engine,
|
|
1247
|
+
organizationId,
|
|
1248
|
+
orgLabel: organizationId ?? "env"
|
|
1249
|
+
});
|
|
1250
|
+
this.overlayRepos.set(key, repo);
|
|
1251
|
+
}
|
|
1252
|
+
return repo;
|
|
1253
|
+
}
|
|
705
1254
|
async ensureOverlayIndex() {
|
|
706
1255
|
if (this.overlayIndexEnsured) return;
|
|
707
1256
|
this.overlayIndexEnsured = true;
|
|
@@ -991,23 +1540,34 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
991
1540
|
}
|
|
992
1541
|
} catch {
|
|
993
1542
|
}
|
|
994
|
-
if (item === void 0) {
|
|
995
|
-
item = this.engine.registry.getItem(request.type, request.name);
|
|
996
|
-
if (item === void 0) {
|
|
997
|
-
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
998
|
-
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
1543
|
if (item === void 0) {
|
|
1002
1544
|
try {
|
|
1003
1545
|
const services = this.getServicesRegistry?.();
|
|
1004
1546
|
const metadataService = services?.get("metadata");
|
|
1005
1547
|
if (metadataService && typeof metadataService.get === "function") {
|
|
1006
|
-
|
|
1548
|
+
const fromService = await metadataService.get(request.type, request.name);
|
|
1549
|
+
if (fromService !== void 0 && fromService !== null) {
|
|
1550
|
+
item = fromService;
|
|
1551
|
+
} else {
|
|
1552
|
+
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
1553
|
+
if (alt) {
|
|
1554
|
+
const altFromService = await metadataService.get(alt, request.name);
|
|
1555
|
+
if (altFromService !== void 0 && altFromService !== null) {
|
|
1556
|
+
item = altFromService;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1007
1560
|
}
|
|
1008
1561
|
} catch {
|
|
1009
1562
|
}
|
|
1010
1563
|
}
|
|
1564
|
+
if (item === void 0) {
|
|
1565
|
+
item = this.engine.registry.getItem(request.type, request.name);
|
|
1566
|
+
if (item === void 0) {
|
|
1567
|
+
const alt = import_shared.PLURAL_TO_SINGULAR[request.type] ?? import_shared.SINGULAR_TO_PLURAL[request.type];
|
|
1568
|
+
if (alt) item = this.engine.registry.getItem(alt, request.name);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1011
1571
|
return {
|
|
1012
1572
|
type: request.type,
|
|
1013
1573
|
name: request.name,
|
|
@@ -1261,6 +1821,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1261
1821
|
};
|
|
1262
1822
|
}
|
|
1263
1823
|
async updateData(request) {
|
|
1824
|
+
await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
|
|
1264
1825
|
const opts = { where: { id: request.id } };
|
|
1265
1826
|
if (request.context !== void 0) opts.context = request.context;
|
|
1266
1827
|
const result = await this.engine.update(request.object, request.data, opts);
|
|
@@ -1271,6 +1832,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1271
1832
|
};
|
|
1272
1833
|
}
|
|
1273
1834
|
async deleteData(request) {
|
|
1835
|
+
await this.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context);
|
|
1274
1836
|
const opts = { where: { id: request.id } };
|
|
1275
1837
|
if (request.context !== void 0) opts.context = request.context;
|
|
1276
1838
|
await this.engine.delete(request.object, opts);
|
|
@@ -1280,6 +1842,42 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1280
1842
|
success: true
|
|
1281
1843
|
};
|
|
1282
1844
|
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Optimistic Concurrency Control gate shared by updateData/deleteData.
|
|
1847
|
+
*
|
|
1848
|
+
* When the caller passes a non-empty `expectedVersion` token (typically
|
|
1849
|
+
* the `updated_at` value they read), this fetches the current record
|
|
1850
|
+
* and compares its `updated_at` against the token. Mismatch → throw
|
|
1851
|
+
* `ConcurrentUpdateError` which the REST layer maps to 409.
|
|
1852
|
+
*
|
|
1853
|
+
* Behaviour:
|
|
1854
|
+
* - Empty/missing token → no check (opt-in semantics; existing callers
|
|
1855
|
+
* that haven't yet adopted OCC are unaffected).
|
|
1856
|
+
* - Record not found → no check; downstream `engine.update` will
|
|
1857
|
+
* surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
|
|
1858
|
+
* treat "missing record" as a concurrency conflict.
|
|
1859
|
+
* - Record has no `updated_at` field (timestamps disabled) → no check.
|
|
1860
|
+
* Logging would be noisy here; OCC is opt-in and the absence of a
|
|
1861
|
+
* version column is an explicit "this object doesn't support OCC"
|
|
1862
|
+
* signal.
|
|
1863
|
+
*/
|
|
1864
|
+
async assertVersionMatch(object, id, expectedVersion, context) {
|
|
1865
|
+
const expected = normaliseVersionToken(expectedVersion);
|
|
1866
|
+
if (!expected) return;
|
|
1867
|
+
const findOpts = { where: { id } };
|
|
1868
|
+
if (context !== void 0) findOpts.context = context;
|
|
1869
|
+
const current = await this.engine.findOne(object, findOpts);
|
|
1870
|
+
if (!current) return;
|
|
1871
|
+
const currentVersion = normaliseVersionToken(current.updated_at);
|
|
1872
|
+
if (!currentVersion) return;
|
|
1873
|
+
if (currentVersion !== expected) {
|
|
1874
|
+
throw new ConcurrentUpdateError({
|
|
1875
|
+
currentVersion,
|
|
1876
|
+
currentRecord: current,
|
|
1877
|
+
message: `Record ${object}/${id} was modified by another user (current version ${currentVersion}, expected ${expected})`
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1283
1881
|
// ==========================================
|
|
1284
1882
|
// Global Search (M10.5)
|
|
1285
1883
|
// ==========================================
|
|
@@ -1852,6 +2450,27 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1852
2450
|
const singular = import_shared.PLURAL_TO_SINGULAR[type] ?? type;
|
|
1853
2451
|
return _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES.has(singular) || _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES.has(type);
|
|
1854
2452
|
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Mirror an object-type overlay write into the in-memory engine
|
|
2455
|
+
* registry so subsequent CRUD finds the new schema. Idempotent and
|
|
2456
|
+
* safe to call after a successful persistence call. For the legacy
|
|
2457
|
+
* write path this is invoked BEFORE persistence (historical behavior
|
|
2458
|
+
* preserved); for the PR-10d.3 repository path it is invoked only
|
|
2459
|
+
* AFTER `put()` resolves successfully, so a failed write — DB error,
|
|
2460
|
+
* optimistic-lock conflict, validation failure — never leaks a
|
|
2461
|
+
* stale schema into the registry.
|
|
2462
|
+
*/
|
|
2463
|
+
applyObjectRegistryMutation(request) {
|
|
2464
|
+
if (request.type !== "object" && request.type !== "objects") return;
|
|
2465
|
+
this.engine.registry.registerItem(request.type, request.item, "name");
|
|
2466
|
+
try {
|
|
2467
|
+
this.engine.registry.registerObject(request.item, "sys_metadata");
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
console.warn(
|
|
2470
|
+
`[Protocol] registerObject failed for ${request.name}: ${err?.message ?? err}`
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
1855
2474
|
async saveMetaItem(request) {
|
|
1856
2475
|
if (!request.item) {
|
|
1857
2476
|
throw new Error("Item data is required");
|
|
@@ -1886,17 +2505,51 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1886
2505
|
}
|
|
1887
2506
|
}
|
|
1888
2507
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
2508
|
+
await this.ensureOverlayIndex();
|
|
2509
|
+
const singularTypeForRepo = import_shared.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
2510
|
+
if (_ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo)) {
|
|
2511
|
+
const orgId = request.organizationId ?? null;
|
|
2512
|
+
const repo = this.getOverlayRepo(orgId);
|
|
2513
|
+
const ref = {
|
|
2514
|
+
type: singularTypeForRepo,
|
|
2515
|
+
name: request.name,
|
|
2516
|
+
org: orgId ?? "env"
|
|
2517
|
+
};
|
|
2518
|
+
let parentVersion;
|
|
2519
|
+
if (request.parentVersion !== void 0) {
|
|
2520
|
+
parentVersion = request.parentVersion;
|
|
2521
|
+
} else {
|
|
2522
|
+
const current = await repo.get(ref);
|
|
2523
|
+
parentVersion = current?.hash ?? null;
|
|
2524
|
+
}
|
|
1891
2525
|
try {
|
|
1892
|
-
|
|
2526
|
+
const result = await repo.put(ref, request.item, {
|
|
2527
|
+
parentVersion,
|
|
2528
|
+
actor: request.actor ?? "system",
|
|
2529
|
+
source: "protocol.saveMetaItem"
|
|
2530
|
+
});
|
|
2531
|
+
this.applyObjectRegistryMutation(request);
|
|
2532
|
+
return {
|
|
2533
|
+
success: true,
|
|
2534
|
+
version: result.version,
|
|
2535
|
+
seq: result.seq,
|
|
2536
|
+
message: orgId ? `Saved customization overlay (org=${orgId}) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]` : `Saved customization overlay (env-wide) \u2014 type=${request.type}, name=${request.name} [seq=${result.seq}]`
|
|
2537
|
+
};
|
|
1893
2538
|
} catch (err) {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
2539
|
+
if (err instanceof import_metadata_core2.ConflictError) {
|
|
2540
|
+
const conflict = new Error(
|
|
2541
|
+
`[metadata_conflict] ${request.type}/${request.name} has been modified since you loaded it. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
|
|
2542
|
+
);
|
|
2543
|
+
conflict.code = "metadata_conflict";
|
|
2544
|
+
conflict.status = 409;
|
|
2545
|
+
conflict.expectedParent = err.expectedParent;
|
|
2546
|
+
conflict.actualHead = err.actualHead;
|
|
2547
|
+
throw conflict;
|
|
2548
|
+
}
|
|
2549
|
+
throw err;
|
|
1897
2550
|
}
|
|
1898
2551
|
}
|
|
1899
|
-
|
|
2552
|
+
this.applyObjectRegistryMutation(request);
|
|
1900
2553
|
try {
|
|
1901
2554
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1902
2555
|
const orgId = request.organizationId ?? null;
|
|
@@ -1953,6 +2606,35 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1953
2606
|
throw err;
|
|
1954
2607
|
}
|
|
1955
2608
|
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Yield the durable change-log for a single metadata item — every
|
|
2611
|
+
* put/delete recorded in `sys_metadata_history` for `(org, type, name)`,
|
|
2612
|
+
* in event_seq order. Powers the Studio "History" tab and any
|
|
2613
|
+
* client-side audit timeline.
|
|
2614
|
+
*
|
|
2615
|
+
* Returns `[]` for non-overlay-allowed types (the legacy raw-engine
|
|
2616
|
+
* path doesn't record history) instead of throwing — callers can treat
|
|
2617
|
+
* "no history" uniformly.
|
|
2618
|
+
*/
|
|
2619
|
+
async historyMetaItem(request) {
|
|
2620
|
+
const singularType = import_shared.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
2621
|
+
if (!_ObjectStackProtocolImplementation.isOverlayAllowed(singularType)) {
|
|
2622
|
+
return { events: [] };
|
|
2623
|
+
}
|
|
2624
|
+
const orgId = request.organizationId ?? null;
|
|
2625
|
+
const repo = this.getOverlayRepo(orgId);
|
|
2626
|
+
const ref = {
|
|
2627
|
+
type: singularType,
|
|
2628
|
+
name: request.name,
|
|
2629
|
+
org: orgId ?? "env"
|
|
2630
|
+
};
|
|
2631
|
+
const events = [];
|
|
2632
|
+
const opts = {};
|
|
2633
|
+
if (request.sinceSeq !== void 0) opts.sinceSeq = request.sinceSeq;
|
|
2634
|
+
if (request.limit !== void 0) opts.limit = request.limit;
|
|
2635
|
+
for await (const ev of repo.history(ref, opts)) events.push(ev);
|
|
2636
|
+
return { events };
|
|
2637
|
+
}
|
|
1956
2638
|
/**
|
|
1957
2639
|
* Remove a customization overlay row for the given metadata item, so the
|
|
1958
2640
|
* next read falls through to the artifact-loaded default. Implements the
|
|
@@ -1968,6 +2650,66 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
1968
2650
|
err.status = 403;
|
|
1969
2651
|
throw err;
|
|
1970
2652
|
}
|
|
2653
|
+
const singularTypeForRepo = import_shared.PLURAL_TO_SINGULAR[request.type] ?? request.type;
|
|
2654
|
+
const useRepoPath = _ObjectStackProtocolImplementation.isOverlayAllowed(singularTypeForRepo);
|
|
2655
|
+
if (useRepoPath) {
|
|
2656
|
+
const orgId = request.organizationId ?? null;
|
|
2657
|
+
const repo = this.getOverlayRepo(orgId);
|
|
2658
|
+
const ref = {
|
|
2659
|
+
type: singularTypeForRepo,
|
|
2660
|
+
name: request.name,
|
|
2661
|
+
org: orgId ?? "env"
|
|
2662
|
+
};
|
|
2663
|
+
try {
|
|
2664
|
+
const current = await repo.get(ref);
|
|
2665
|
+
if (!current) {
|
|
2666
|
+
return {
|
|
2667
|
+
success: true,
|
|
2668
|
+
reset: false,
|
|
2669
|
+
message: `No customization overlay found for ${request.type}/${request.name} \u2014 already at artifact default.`
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
const parentVersion = request.parentVersion !== void 0 ? request.parentVersion ?? current.hash : current.hash;
|
|
2673
|
+
const result = await repo.delete(ref, {
|
|
2674
|
+
parentVersion,
|
|
2675
|
+
actor: request.actor ?? "system",
|
|
2676
|
+
source: "protocol.deleteMetaItem"
|
|
2677
|
+
});
|
|
2678
|
+
if (this.projectId === void 0) {
|
|
2679
|
+
try {
|
|
2680
|
+
const services = this.getServicesRegistry?.();
|
|
2681
|
+
const metadataService = services?.get("metadata");
|
|
2682
|
+
if (metadataService && typeof metadataService.get === "function") {
|
|
2683
|
+
const artifactItem = await metadataService.get(request.type, request.name);
|
|
2684
|
+
if (artifactItem !== void 0) {
|
|
2685
|
+
this.engine.registry.registerItem(request.type, artifactItem, "name");
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
} catch {
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return {
|
|
2692
|
+
success: true,
|
|
2693
|
+
reset: true,
|
|
2694
|
+
seq: result.seq,
|
|
2695
|
+
message: `Customization overlay deleted \u2014 ${request.type}/${request.name} reset to artifact default. [seq=${result.seq}]`
|
|
2696
|
+
};
|
|
2697
|
+
} catch (err) {
|
|
2698
|
+
if (err instanceof import_metadata_core2.ConflictError) {
|
|
2699
|
+
const conflict = new Error(
|
|
2700
|
+
`[metadata_conflict] ${request.type}/${request.name} has been modified since you loaded it. Expected parent ${err.expectedParent ?? "null"} but current is ${err.actualHead ?? "null"}.`
|
|
2701
|
+
);
|
|
2702
|
+
conflict.code = "metadata_conflict";
|
|
2703
|
+
conflict.status = 409;
|
|
2704
|
+
conflict.expectedParent = err.expectedParent;
|
|
2705
|
+
conflict.actualHead = err.actualHead;
|
|
2706
|
+
throw conflict;
|
|
2707
|
+
}
|
|
2708
|
+
const e = new Error(`Failed to delete customization overlay: ${err.message ?? err}`);
|
|
2709
|
+
e.status = err?.status ?? 500;
|
|
2710
|
+
throw e;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
1971
2713
|
const scopedWhere = {
|
|
1972
2714
|
type: request.type,
|
|
1973
2715
|
name: request.name,
|
|
@@ -2191,7 +2933,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
|
|
|
2191
2933
|
*/
|
|
2192
2934
|
_ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
2193
2935
|
const out = /* @__PURE__ */ new Set();
|
|
2194
|
-
for (const entry of
|
|
2936
|
+
for (const entry of import_kernel3.DEFAULT_METADATA_TYPE_REGISTRY) {
|
|
2195
2937
|
if (!entry.allowOrgOverride) continue;
|
|
2196
2938
|
out.add(entry.type);
|
|
2197
2939
|
const plural = import_shared.SINGULAR_TO_PLURAL[entry.type];
|
|
@@ -2202,7 +2944,7 @@ _ObjectStackProtocolImplementation.OVERLAY_ALLOWED_TYPES = (() => {
|
|
|
2202
2944
|
var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
|
|
2203
2945
|
|
|
2204
2946
|
// src/engine.ts
|
|
2205
|
-
var
|
|
2947
|
+
var import_kernel4 = require("@objectstack/spec/kernel");
|
|
2206
2948
|
var import_core = require("@objectstack/core");
|
|
2207
2949
|
var import_system = require("@objectstack/spec/system");
|
|
2208
2950
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
@@ -4490,7 +5232,7 @@ var _ObjectQL = class _ObjectQL {
|
|
|
4490
5232
|
*/
|
|
4491
5233
|
createContext(ctx) {
|
|
4492
5234
|
return new ScopedContext(
|
|
4493
|
-
|
|
5235
|
+
import_kernel4.ExecutionContextSchema.parse(ctx),
|
|
4494
5236
|
this
|
|
4495
5237
|
);
|
|
4496
5238
|
}
|
|
@@ -4778,6 +5520,8 @@ var ObjectQLPlugin = class {
|
|
|
4778
5520
|
*/
|
|
4779
5521
|
this.startupTimeout = 12e4;
|
|
4780
5522
|
this.skipSchemaSync = false;
|
|
5523
|
+
/** Unsubscribe handles for metadata-event subscriptions (ADR-0008 PR-7). */
|
|
5524
|
+
this.metadataUnsubscribes = [];
|
|
4781
5525
|
this.init = async (ctx) => {
|
|
4782
5526
|
if (!this.ql) {
|
|
4783
5527
|
const hostCtx = { ...this.hostContext, logger: ctx.logger };
|
|
@@ -4824,6 +5568,9 @@ var ObjectQLPlugin = class {
|
|
|
4824
5568
|
if (metadataService && typeof metadataService.loadMany === "function" && this.ql) {
|
|
4825
5569
|
await this.loadMetadataFromService(metadataService, ctx);
|
|
4826
5570
|
}
|
|
5571
|
+
if (metadataService && typeof metadataService.subscribe === "function" && this.ql) {
|
|
5572
|
+
this.subscribeToMetadataEvents(metadataService, ctx);
|
|
5573
|
+
}
|
|
4827
5574
|
} catch (e) {
|
|
4828
5575
|
ctx.logger.debug("No external metadata service to sync from");
|
|
4829
5576
|
}
|
|
@@ -4877,6 +5624,16 @@ var ObjectQLPlugin = class {
|
|
|
4877
5624
|
objectsRegistered: this.ql?.registry?.getAllObjects?.()?.length || 0
|
|
4878
5625
|
});
|
|
4879
5626
|
};
|
|
5627
|
+
this.stop = async (ctx) => {
|
|
5628
|
+
for (const unsub of this.metadataUnsubscribes) {
|
|
5629
|
+
try {
|
|
5630
|
+
unsub();
|
|
5631
|
+
} catch (e) {
|
|
5632
|
+
ctx.logger.debug("[ObjectQLPlugin] metadata-event unsubscribe failed", { error: e?.message });
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
this.metadataUnsubscribes = [];
|
|
5636
|
+
};
|
|
4880
5637
|
if (qlOrOptions instanceof ObjectQL) {
|
|
4881
5638
|
this.ql = qlOrOptions;
|
|
4882
5639
|
this.hostContext = hostContext;
|
|
@@ -4893,6 +5650,62 @@ var ObjectQLPlugin = class {
|
|
|
4893
5650
|
}
|
|
4894
5651
|
this.skipSchemaSync = typeof opts.skipSchemaSync === "boolean" ? opts.skipSchemaSync : process.env.OS_SKIP_SCHEMA_SYNC === "1";
|
|
4895
5652
|
}
|
|
5653
|
+
/**
|
|
5654
|
+
* Subscribe to `object` metadata events from the metadata service and
|
|
5655
|
+
* invalidate the SchemaRegistry merge cache on each event (ADR-0008
|
|
5656
|
+
* PR-7). For create/update we also re-load the affected object from
|
|
5657
|
+
* the metadata service so subsequent reads see the new definition;
|
|
5658
|
+
* for delete we unregister it from every contributing package.
|
|
5659
|
+
*
|
|
5660
|
+
* Events are filtered to the canonical `object` type — view/dashboard
|
|
5661
|
+
* /flow edits go through their own consumers (Studio SSE, REST cache).
|
|
5662
|
+
*
|
|
5663
|
+
* Stored unsubscribe handle is invoked from {@link stop}.
|
|
5664
|
+
*/
|
|
5665
|
+
subscribeToMetadataEvents(metadataService, ctx) {
|
|
5666
|
+
const handler = async (evt) => {
|
|
5667
|
+
if (!this.ql) return;
|
|
5668
|
+
const name = evt?.name ?? "";
|
|
5669
|
+
if (!name) return;
|
|
5670
|
+
const eventType = evt?.type === "added" || evt?.type === "changed" || evt?.type === "deleted" ? evt.type : "changed";
|
|
5671
|
+
try {
|
|
5672
|
+
this.ql.registry.invalidate(name);
|
|
5673
|
+
if (eventType === "deleted") {
|
|
5674
|
+
ctx.logger.info("[ObjectQLPlugin] object metadata deleted \u2014 registry invalidated", { name });
|
|
5675
|
+
return;
|
|
5676
|
+
}
|
|
5677
|
+
const fresh = typeof metadataService.get === "function" ? await metadataService.get("object", name) : void 0;
|
|
5678
|
+
if (fresh && typeof fresh === "object") {
|
|
5679
|
+
const packageId = fresh._packageId ?? "metadata-service";
|
|
5680
|
+
const namespace = fresh.namespace;
|
|
5681
|
+
this.ql.registry.registerObject(
|
|
5682
|
+
fresh,
|
|
5683
|
+
packageId,
|
|
5684
|
+
namespace,
|
|
5685
|
+
"own"
|
|
5686
|
+
);
|
|
5687
|
+
ctx.logger.info("[ObjectQLPlugin] object metadata updated \u2014 registry refreshed", {
|
|
5688
|
+
name,
|
|
5689
|
+
packageId
|
|
5690
|
+
});
|
|
5691
|
+
} else {
|
|
5692
|
+
ctx.logger.debug("[ObjectQLPlugin] object event received but metadata service has no fresh body", { name });
|
|
5693
|
+
}
|
|
5694
|
+
} catch (e) {
|
|
5695
|
+
ctx.logger.warn("[ObjectQLPlugin] metadata event handler failed", {
|
|
5696
|
+
name,
|
|
5697
|
+
error: e?.message
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
};
|
|
5701
|
+
const unsub = metadataService.subscribe("object", handler);
|
|
5702
|
+
if (typeof unsub === "function") {
|
|
5703
|
+
this.metadataUnsubscribes.push(unsub);
|
|
5704
|
+
} else if (unsub && typeof unsub.unsubscribe === "function") {
|
|
5705
|
+
this.metadataUnsubscribes.push(() => unsub.unsubscribe());
|
|
5706
|
+
}
|
|
5707
|
+
ctx.logger.info("[ObjectQLPlugin] subscribed to object metadata events (ADR-0008 PR-7)");
|
|
5708
|
+
}
|
|
4896
5709
|
/**
|
|
4897
5710
|
* Register built-in audit hooks for auto-stamping created_by/updated_by
|
|
4898
5711
|
* and fetching previousData for update/delete operations. These are
|
|
@@ -5391,6 +6204,7 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
|
|
|
5391
6204
|
RESERVED_NAMESPACES,
|
|
5392
6205
|
SchemaRegistry,
|
|
5393
6206
|
ScopedContext,
|
|
6207
|
+
SysMetadataRepository,
|
|
5394
6208
|
ValidationError,
|
|
5395
6209
|
applyInMemoryAggregation,
|
|
5396
6210
|
applySystemFields,
|