@objectstack/metadata 17.0.0-rc.1 → 17.0.0-rc.2

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.cts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as System from '@objectstack/spec/system';
2
2
  import { MetadataFormat, MetadataLoaderContract, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult, MetadataWatchEvent, MetadataManagerConfig, PackagePublishResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataDiffResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy } from '@objectstack/spec/system';
3
- export { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
3
+ export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
4
4
  import { IMetadataService, IDataDriver, IDataEngine, IRealtimeService, MetadataWriteOptions, MetadataWatchCallback, MetadataWatchHandle, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, IPubSub, ISchemaDriver } from '@objectstack/spec/contracts';
5
- export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
5
+ export { IMetadataService, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
6
6
  import { MetadataTypeRegistryEntry, MetadataQuery, MetadataQueryResult, MetadataBulkResult, MetadataOverlay, MetadataValidationResult, MetadataDependency, MetadataPluginConfig } from '@objectstack/spec/kernel';
7
7
  export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
8
8
  import { Logger, Plugin, PluginContext } from '@objectstack/core';
@@ -199,6 +199,28 @@ declare class MetadataManager implements IMetadataService {
199
199
  * @param service - An IRealtimeService instance for event publishing
200
200
  */
201
201
  setRealtimeService(service: IRealtimeService): void;
202
+ /**
203
+ * Publish a realtime {@link RealtimeMetadataEvent} for a metadata write
204
+ * (#4602 — contract-first).
205
+ *
206
+ * What reaches a `subscribeMetadata` callback must BE the spec's
207
+ * `MetadataEvent` (`@objectstack/spec/api`): `id` (uuid) at the top level,
208
+ * flattened `metadataType`/`name`/`definition`, `userId` when the write
209
+ * carried an actor. The transport keeps its `RealtimeEventPayload`
210
+ * envelope — `payload` carries the complete `MetadataEvent`, and the client
211
+ * SDK unwraps + validates it at the boundary.
212
+ *
213
+ * Two loud-by-design gates:
214
+ * - `MetadataEventType` is a CLOSED enum. A metadata type outside it has
215
+ * no declared realtime event contract, so we skip publishing (debug log)
216
+ * instead of emitting an event every compliant consumer must reject.
217
+ * Declared = enforced; widening coverage means widening the spec enum,
218
+ * not producing off-contract events.
219
+ * - The event body is `MetadataEventSchema.parse`d before publish, so a
220
+ * malformed producer fails here (warn log, event not published) rather
221
+ * than delivering a lie downstream.
222
+ */
223
+ private publishRealtimeMetadataEvent;
202
224
  /**
203
225
  * Register a new metadata loader (data source)
204
226
  */
@@ -660,6 +682,14 @@ declare class MetadataPlugin implements Plugin {
660
682
  * initializes.
661
683
  */
662
684
  providesServices: string[];
685
+ /**
686
+ * init() registers the metadata system objects through the `manifest`
687
+ * service ObjectQLPlugin provides — order-if-present so that
688
+ * registration is deterministic instead of "whichever init ran first"
689
+ * (ADR-0116, #4471). Soft, not hard: without an engine the plugin
690
+ * degrades on purpose (objects are discovered via the legacy fallback).
691
+ */
692
+ optionalDependencies: string[];
663
693
  private manager;
664
694
  private options;
665
695
  private repository?;
@@ -897,6 +927,19 @@ declare class DatabaseLoader implements MetadataLoader {
897
927
  private trackHistory;
898
928
  private schemaReady;
899
929
  private historySchemaReady;
930
+ /**
931
+ * Whether the loud "DDL failed" report has already been printed for the
932
+ * metadata table / history table respectively. AGENTS.md → "Degradation log
933
+ * levels": say it **once**, at the first degradation, not once per retry.
934
+ */
935
+ private schemaFailureReported;
936
+ private historySchemaFailureReported;
937
+ /**
938
+ * Same once-only discipline for the #4825 seam: the history table is readable
939
+ * or it is not, and repeating the report per skipped write turns a real
940
+ * degradation into noise people learn to skim.
941
+ */
942
+ private historySeqFailureReported;
900
943
  /** (type, name) → metadata payload — primes `load()` */
901
944
  private readonly loadCache?;
902
945
  /** type → array of payloads — primes `loadMany()` */
@@ -934,6 +977,24 @@ declare class DatabaseLoader implements MetadataLoader {
934
977
  * Reads `MAX(event_seq) + 1` for the configured `organization_id`.
935
978
  * Legacy path — not transactional, so concurrent writes can collide.
936
979
  * The canonical (transactional) producer is `SysMetadataRepository`.
980
+ *
981
+ * #4825 (same shape as #4728, rule from #4632) — discriminate by error TYPE.
982
+ * This used to `catch { return 1 }`, with a comment that named BOTH reasons a
983
+ * read can fail and then answered both the same way. Exactly one of them is
984
+ * benign: the history table has not been provisioned, so there is no row to
985
+ * be inconsistent with and 1 genuinely IS the next number. Every other reason
986
+ * — connection drop, timeout, insufficient privileges — means the rows are
987
+ * still there and simply were not seen, and answering 1 against a table with
988
+ * N rows **collides with existing rows**: the insert succeeds, the log stays
989
+ * empty, and `event_seq` (the ordering key that history listing and rollback
990
+ * targeting both stand on) is silently wrong from then on. Note this is the
991
+ * costlier half of the #4728 family — not bytes that never landed, but bytes
992
+ * that landed *wrong*, which no retry and no restart repairs.
993
+ *
994
+ * @throws The underlying driver error, unchanged, for every non-benign read
995
+ * failure. Deliberate: a sequence number this method cannot derive
996
+ * from data it actually read is not a number it may invent. The
997
+ * caller ({@link createHistoryRecord}) owns the consequence.
937
998
  */
938
999
  private nextEventSeq;
939
1000
  /**
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as System from '@objectstack/spec/system';
2
2
  import { MetadataFormat, MetadataLoaderContract, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult, MetadataWatchEvent, MetadataManagerConfig, PackagePublishResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataDiffResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy } from '@objectstack/spec/system';
3
- export { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
3
+ export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
4
4
  import { IMetadataService, IDataDriver, IDataEngine, IRealtimeService, MetadataWriteOptions, MetadataWatchCallback, MetadataWatchHandle, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, IPubSub, ISchemaDriver } from '@objectstack/spec/contracts';
5
- export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
5
+ export { IMetadataService, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
6
6
  import { MetadataTypeRegistryEntry, MetadataQuery, MetadataQueryResult, MetadataBulkResult, MetadataOverlay, MetadataValidationResult, MetadataDependency, MetadataPluginConfig } from '@objectstack/spec/kernel';
7
7
  export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
8
8
  import { Logger, Plugin, PluginContext } from '@objectstack/core';
@@ -199,6 +199,28 @@ declare class MetadataManager implements IMetadataService {
199
199
  * @param service - An IRealtimeService instance for event publishing
200
200
  */
201
201
  setRealtimeService(service: IRealtimeService): void;
202
+ /**
203
+ * Publish a realtime {@link RealtimeMetadataEvent} for a metadata write
204
+ * (#4602 — contract-first).
205
+ *
206
+ * What reaches a `subscribeMetadata` callback must BE the spec's
207
+ * `MetadataEvent` (`@objectstack/spec/api`): `id` (uuid) at the top level,
208
+ * flattened `metadataType`/`name`/`definition`, `userId` when the write
209
+ * carried an actor. The transport keeps its `RealtimeEventPayload`
210
+ * envelope — `payload` carries the complete `MetadataEvent`, and the client
211
+ * SDK unwraps + validates it at the boundary.
212
+ *
213
+ * Two loud-by-design gates:
214
+ * - `MetadataEventType` is a CLOSED enum. A metadata type outside it has
215
+ * no declared realtime event contract, so we skip publishing (debug log)
216
+ * instead of emitting an event every compliant consumer must reject.
217
+ * Declared = enforced; widening coverage means widening the spec enum,
218
+ * not producing off-contract events.
219
+ * - The event body is `MetadataEventSchema.parse`d before publish, so a
220
+ * malformed producer fails here (warn log, event not published) rather
221
+ * than delivering a lie downstream.
222
+ */
223
+ private publishRealtimeMetadataEvent;
202
224
  /**
203
225
  * Register a new metadata loader (data source)
204
226
  */
@@ -660,6 +682,14 @@ declare class MetadataPlugin implements Plugin {
660
682
  * initializes.
661
683
  */
662
684
  providesServices: string[];
685
+ /**
686
+ * init() registers the metadata system objects through the `manifest`
687
+ * service ObjectQLPlugin provides — order-if-present so that
688
+ * registration is deterministic instead of "whichever init ran first"
689
+ * (ADR-0116, #4471). Soft, not hard: without an engine the plugin
690
+ * degrades on purpose (objects are discovered via the legacy fallback).
691
+ */
692
+ optionalDependencies: string[];
663
693
  private manager;
664
694
  private options;
665
695
  private repository?;
@@ -897,6 +927,19 @@ declare class DatabaseLoader implements MetadataLoader {
897
927
  private trackHistory;
898
928
  private schemaReady;
899
929
  private historySchemaReady;
930
+ /**
931
+ * Whether the loud "DDL failed" report has already been printed for the
932
+ * metadata table / history table respectively. AGENTS.md → "Degradation log
933
+ * levels": say it **once**, at the first degradation, not once per retry.
934
+ */
935
+ private schemaFailureReported;
936
+ private historySchemaFailureReported;
937
+ /**
938
+ * Same once-only discipline for the #4825 seam: the history table is readable
939
+ * or it is not, and repeating the report per skipped write turns a real
940
+ * degradation into noise people learn to skim.
941
+ */
942
+ private historySeqFailureReported;
900
943
  /** (type, name) → metadata payload — primes `load()` */
901
944
  private readonly loadCache?;
902
945
  /** type → array of payloads — primes `loadMany()` */
@@ -934,6 +977,24 @@ declare class DatabaseLoader implements MetadataLoader {
934
977
  * Reads `MAX(event_seq) + 1` for the configured `organization_id`.
935
978
  * Legacy path — not transactional, so concurrent writes can collide.
936
979
  * The canonical (transactional) producer is `SysMetadataRepository`.
980
+ *
981
+ * #4825 (same shape as #4728, rule from #4632) — discriminate by error TYPE.
982
+ * This used to `catch { return 1 }`, with a comment that named BOTH reasons a
983
+ * read can fail and then answered both the same way. Exactly one of them is
984
+ * benign: the history table has not been provisioned, so there is no row to
985
+ * be inconsistent with and 1 genuinely IS the next number. Every other reason
986
+ * — connection drop, timeout, insufficient privileges — means the rows are
987
+ * still there and simply were not seen, and answering 1 against a table with
988
+ * N rows **collides with existing rows**: the insert succeeds, the log stays
989
+ * empty, and `event_seq` (the ordering key that history listing and rollback
990
+ * targeting both stand on) is silently wrong from then on. Note this is the
991
+ * costlier half of the #4728 family — not bytes that never landed, but bytes
992
+ * that landed *wrong*, which no retry and no restart repairs.
993
+ *
994
+ * @throws The underlying driver error, unchanged, for every non-benign read
995
+ * failure. Deliberate: a sequence number this method cannot derive
996
+ * from data it actually read is not a number it may invent. The
997
+ * caller ({@link createHistoryRecord}) owns the consequence.
937
998
  */
938
999
  private nextEventSeq;
939
1000
  /**
package/dist/index.js CHANGED
@@ -169,6 +169,10 @@ var init_hmr_routes = __esm({
169
169
 
170
170
  // src/metadata-manager.ts
171
171
  import { getMetadataTypeActions } from "@objectstack/spec/kernel";
172
+ import {
173
+ MetadataEventType,
174
+ MetadataEventSchema
175
+ } from "@objectstack/spec/api";
172
176
  import { createLogger } from "@objectstack/core";
173
177
 
174
178
  // src/serializers/json-serializer.ts
@@ -507,6 +511,70 @@ var LRUCache = class {
507
511
  }
508
512
  };
509
513
 
514
+ // src/utils/schema-sync-errors.ts
515
+ var ALREADY_EXISTS = {
516
+ codes: /* @__PURE__ */ new Set([
517
+ // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
518
+ "42P07",
519
+ // duplicate_table
520
+ "42701",
521
+ // duplicate_column
522
+ "42710",
523
+ // duplicate_object — index / constraint already exists
524
+ // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
525
+ "ER_TABLE_EXISTS_ERROR",
526
+ // 1050
527
+ "ER_DUP_FIELDNAME",
528
+ // 1060
529
+ "ER_DUP_KEYNAME"
530
+ // 1061
531
+ ]),
532
+ errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
533
+ /**
534
+ * Message fallback for drivers that carry no machine-readable code —
535
+ * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
536
+ * every DDL failure, so the message is the only signal available:
537
+ * - `table sys_metadata already exists`
538
+ * - `duplicate column name: environment_id`
539
+ * - `index idx_x already exists`
540
+ * Postgres phrases its own as `relation "x" already exists` /
541
+ * `column "x" of relation "y" already exists`, which matches the same test.
542
+ */
543
+ message: /already exists|duplicate column name|duplicate key name/i
544
+ };
545
+ var MISSING_TABLE = {
546
+ codes: /* @__PURE__ */ new Set([
547
+ "42P01",
548
+ // PostgreSQL undefined_table
549
+ "ER_NO_SUCH_TABLE"
550
+ // MySQL / MariaDB 1146
551
+ ]),
552
+ errnos: /* @__PURE__ */ new Set([1146]),
553
+ /**
554
+ * - SQLite / libsql: `no such table: sys_metadata_history`
555
+ * - PostgreSQL: `relation "sys_metadata_history" does not exist`
556
+ * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
557
+ */
558
+ message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i
559
+ };
560
+ var MAX_CAUSE_DEPTH = 4;
561
+ function matchesDriverError(error, signature, depth) {
562
+ if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
563
+ if (typeof error === "string") return signature.message.test(error);
564
+ if (typeof error !== "object") return false;
565
+ const err = error;
566
+ if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
567
+ if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
568
+ if (typeof err.message === "string" && signature.message.test(err.message)) return true;
569
+ return matchesDriverError(err.cause, signature, depth + 1);
570
+ }
571
+ function isSchemaAlreadyExistsError(error, depth = 0) {
572
+ return matchesDriverError(error, ALREADY_EXISTS, depth);
573
+ }
574
+ function isMissingTableError(error, depth = 0) {
575
+ return matchesDriverError(error, MISSING_TABLE, depth);
576
+ }
577
+
510
578
  // src/migrations/add-sys-metadata-overlay-index.ts
511
579
  var INDEX_NAME = "idx_sys_metadata_overlay_active";
512
580
  var TABLE = "sys_metadata";
@@ -617,6 +685,19 @@ var DatabaseLoader = class {
617
685
  };
618
686
  this.schemaReady = false;
619
687
  this.historySchemaReady = false;
688
+ /**
689
+ * Whether the loud "DDL failed" report has already been printed for the
690
+ * metadata table / history table respectively. AGENTS.md → "Degradation log
691
+ * levels": say it **once**, at the first degradation, not once per retry.
692
+ */
693
+ this.schemaFailureReported = false;
694
+ this.historySchemaFailureReported = false;
695
+ /**
696
+ * Same once-only discipline for the #4825 seam: the history table is readable
697
+ * or it is not, and repeating the report per skipped write turns a real
698
+ * degradation into noise people learn to skim.
699
+ */
700
+ this.historySeqFailureReported = false;
620
701
  /**
621
702
  * Once-per-process dedupe for stored-row conversion notices — `load` /
622
703
  * `loadMany` are hot read paths (cached, but re-hit on every TTL expiry),
@@ -726,6 +807,24 @@ var DatabaseLoader = class {
726
807
  * Reads `MAX(event_seq) + 1` for the configured `organization_id`.
727
808
  * Legacy path — not transactional, so concurrent writes can collide.
728
809
  * The canonical (transactional) producer is `SysMetadataRepository`.
810
+ *
811
+ * #4825 (same shape as #4728, rule from #4632) — discriminate by error TYPE.
812
+ * This used to `catch { return 1 }`, with a comment that named BOTH reasons a
813
+ * read can fail and then answered both the same way. Exactly one of them is
814
+ * benign: the history table has not been provisioned, so there is no row to
815
+ * be inconsistent with and 1 genuinely IS the next number. Every other reason
816
+ * — connection drop, timeout, insufficient privileges — means the rows are
817
+ * still there and simply were not seen, and answering 1 against a table with
818
+ * N rows **collides with existing rows**: the insert succeeds, the log stays
819
+ * empty, and `event_seq` (the ordering key that history listing and rollback
820
+ * targeting both stand on) is silently wrong from then on. Note this is the
821
+ * costlier half of the #4728 family — not bytes that never landed, but bytes
822
+ * that landed *wrong*, which no retry and no restart repairs.
823
+ *
824
+ * @throws The underlying driver error, unchanged, for every non-benign read
825
+ * failure. Deliberate: a sequence number this method cannot derive
826
+ * from data it actually read is not a number it may invent. The
827
+ * caller ({@link createHistoryRecord}) owns the consequence.
729
828
  */
730
829
  async nextEventSeq() {
731
830
  const where = this.organizationId ? { organization_id: this.organizationId } : {};
@@ -737,8 +836,9 @@ var DatabaseLoader = class {
737
836
  if (v > max) max = v;
738
837
  }
739
838
  return max + 1;
740
- } catch {
741
- return 1;
839
+ } catch (error) {
840
+ if (isMissingTableError(error)) return 1;
841
+ throw error;
742
842
  }
743
843
  }
744
844
  /**
@@ -775,17 +875,32 @@ var DatabaseLoader = class {
775
875
  ...SysMetadataObject,
776
876
  name: this.tableName
777
877
  });
778
- this.schemaReady = true;
779
- try {
780
- await migrateProjectIdToEnvironmentId(this.driver);
781
- } catch {
782
- }
783
- try {
784
- await addSysMetadataOverlayIndex(this.driver);
785
- } catch {
878
+ } catch (error) {
879
+ if (!isSchemaAlreadyExistsError(error)) {
880
+ if (!this.schemaFailureReported) {
881
+ this.schemaFailureReported = true;
882
+ console.error(
883
+ `[Metadata] DDL for the metadata table \`${this.tableName}\` FAILED \u2014 its table/columns were NOT created or altered. Every metadata write from here on (Studio saves, app installs, org overlays) targets storage that may not exist: writes will error out, or silently drop columns on a lenient driver, while the server keeps reporting healthy. This is NOT the benign "already exists" case \u2014 check the datasource/driver error below (insufficient privileges, datasource not connected, incompatible column type), fix it and restart. Schema sync is retried on the next metadata operation, so a transient cause recovers on its own.`,
884
+ error
885
+ );
886
+ }
887
+ return;
786
888
  }
889
+ }
890
+ if (this.schemaFailureReported) {
891
+ this.schemaFailureReported = false;
892
+ console.info(
893
+ `[Metadata] DDL for the metadata table \`${this.tableName}\` succeeded on retry \u2014 metadata writes are durable again.`
894
+ );
895
+ }
896
+ this.schemaReady = true;
897
+ try {
898
+ await migrateProjectIdToEnvironmentId(this.driver);
899
+ } catch {
900
+ }
901
+ try {
902
+ await addSysMetadataOverlayIndex(this.driver);
787
903
  } catch {
788
- this.schemaReady = true;
789
904
  }
790
905
  }
791
906
  /**
@@ -803,9 +918,25 @@ var DatabaseLoader = class {
803
918
  ...SysMetadataHistoryObject,
804
919
  name: this.historyTableName
805
920
  });
921
+ if (this.historySchemaFailureReported) {
922
+ this.historySchemaFailureReported = false;
923
+ console.info(
924
+ `[Metadata] DDL for the metadata history table \`${this.historyTableName}\` succeeded on retry \u2014 change history is being recorded again.`
925
+ );
926
+ }
806
927
  this.historySchemaReady = true;
807
928
  } catch (error) {
808
- console.error("Failed to ensure history schema, will retry on next operation:", error);
929
+ if (isSchemaAlreadyExistsError(error)) {
930
+ this.historySchemaReady = true;
931
+ return;
932
+ }
933
+ if (!this.historySchemaFailureReported) {
934
+ this.historySchemaFailureReported = true;
935
+ console.error(
936
+ `[Metadata] DDL for the metadata history table \`${this.historyTableName}\` FAILED \u2014 its table/columns were NOT created. Metadata change history (versions, diffs, rollback) will NOT be persisted while every metadata write keeps succeeding, so the audit trail silently ends here. Fix the datasource/driver error below and restart; the sync is retried on the next metadata operation.`,
937
+ error
938
+ );
939
+ }
809
940
  }
810
941
  }
811
942
  /**
@@ -846,7 +977,25 @@ var DatabaseLoader = class {
846
977
  }
847
978
  const historyId = generateId();
848
979
  const metadataJson = JSON.stringify(metadata);
849
- const eventSeq = await this.nextEventSeq();
980
+ let eventSeq;
981
+ try {
982
+ eventSeq = await this.nextEventSeq();
983
+ } catch (error) {
984
+ if (!this.historySeqFailureReported) {
985
+ this.historySeqFailureReported = true;
986
+ console.error(
987
+ `[Metadata] Could not read \`${this.historyTableName}\` to determine the next \`event_seq\` \u2014 the history entry for ${type}/${name} was NOT written, and further entries are being skipped while this persists. The metadata write itself SUCCEEDED, so the server keeps looking healthy while its change history silently develops holes: version timelines and rollback targets will be incomplete. The entry is skipped deliberately \u2014 numbering it from 1 (what this code did before #4825) would collide with existing rows and make \`event_seq\` ordering wrong rather than merely incomplete, which nothing detects and no restart repairs. Fix the datasource/driver error below (connection, timeout, privileges); the next metadata write retries and reports recovery.`,
988
+ error
989
+ );
990
+ }
991
+ return;
992
+ }
993
+ if (this.historySeqFailureReported) {
994
+ this.historySeqFailureReported = false;
995
+ console.info(
996
+ `[Metadata] \`${this.historyTableName}\` is readable again \u2014 \`event_seq\` numbering recovered and change history is being recorded again. Entries skipped during the outage are not backfilled.`
997
+ );
998
+ }
850
999
  const historyRecord = {
851
1000
  id: historyId,
852
1001
  name,
@@ -1303,6 +1452,17 @@ function generateId() {
1303
1452
  }
1304
1453
 
1305
1454
  // src/metadata-manager.ts
1455
+ function generateEventUuid() {
1456
+ const c = globalThis.crypto;
1457
+ if (c && typeof c.randomUUID === "function") {
1458
+ return c.randomUUID();
1459
+ }
1460
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
1461
+ const r = Math.random() * 16 | 0;
1462
+ const v = ch === "x" ? r : r & 3 | 8;
1463
+ return v.toString(16);
1464
+ });
1465
+ }
1306
1466
  var _MetadataManager = class _MetadataManager {
1307
1467
  constructor(config) {
1308
1468
  this.loaders = /* @__PURE__ */ new Map();
@@ -1423,6 +1583,60 @@ var _MetadataManager = class _MetadataManager {
1423
1583
  this.realtimeService = service;
1424
1584
  this.logger.info("RealtimeService configured for metadata events");
1425
1585
  }
1586
+ /**
1587
+ * Publish a realtime {@link RealtimeMetadataEvent} for a metadata write
1588
+ * (#4602 — contract-first).
1589
+ *
1590
+ * What reaches a `subscribeMetadata` callback must BE the spec's
1591
+ * `MetadataEvent` (`@objectstack/spec/api`): `id` (uuid) at the top level,
1592
+ * flattened `metadataType`/`name`/`definition`, `userId` when the write
1593
+ * carried an actor. The transport keeps its `RealtimeEventPayload`
1594
+ * envelope — `payload` carries the complete `MetadataEvent`, and the client
1595
+ * SDK unwraps + validates it at the boundary.
1596
+ *
1597
+ * Two loud-by-design gates:
1598
+ * - `MetadataEventType` is a CLOSED enum. A metadata type outside it has
1599
+ * no declared realtime event contract, so we skip publishing (debug log)
1600
+ * instead of emitting an event every compliant consumer must reject.
1601
+ * Declared = enforced; widening coverage means widening the spec enum,
1602
+ * not producing off-contract events.
1603
+ * - The event body is `MetadataEventSchema.parse`d before publish, so a
1604
+ * malformed producer fails here (warn log, event not published) rather
1605
+ * than delivering a lie downstream.
1606
+ */
1607
+ async publishRealtimeMetadataEvent(action, type, name, opts = {}) {
1608
+ if (!this.realtimeService) return;
1609
+ const eventType = `metadata.${type}.${action}`;
1610
+ if (!MetadataEventType.options.includes(eventType)) {
1611
+ this.logger.debug(
1612
+ `Metadata type '${type}' has no declared realtime event type (MetadataEventType) \u2014 skipping publish`,
1613
+ { eventType, name }
1614
+ );
1615
+ return;
1616
+ }
1617
+ try {
1618
+ const event = MetadataEventSchema.parse({
1619
+ id: generateEventUuid(),
1620
+ type: eventType,
1621
+ metadataType: type,
1622
+ name,
1623
+ ...typeof opts.packageId === "string" ? { packageId: opts.packageId } : {},
1624
+ ...opts.definition !== void 0 ? { definition: opts.definition } : {},
1625
+ ...opts.userId ? { userId: opts.userId } : {},
1626
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1627
+ });
1628
+ const envelope = {
1629
+ type: event.type,
1630
+ object: type,
1631
+ payload: { ...event },
1632
+ timestamp: event.timestamp
1633
+ };
1634
+ await this.realtimeService.publish(envelope);
1635
+ this.logger.debug(`Published ${eventType} event`, { name });
1636
+ } catch (error) {
1637
+ this.logger.warn(`Failed to publish metadata event`, { type, name, error });
1638
+ }
1639
+ }
1426
1640
  /**
1427
1641
  * Register a new metadata loader (data source)
1428
1642
  */
@@ -1466,25 +1680,11 @@ var _MetadataManager = class _MetadataManager {
1466
1680
  await loader.save(type, name, data);
1467
1681
  }
1468
1682
  }
1469
- if (this.realtimeService) {
1470
- const event = {
1471
- type: `metadata.${type}.created`,
1472
- object: type,
1473
- payload: {
1474
- metadataType: type,
1475
- name,
1476
- definition: data,
1477
- packageId: data?.packageId
1478
- },
1479
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1480
- };
1481
- try {
1482
- await this.realtimeService.publish(event);
1483
- this.logger.debug(`Published metadata.${type}.created event`, { name });
1484
- } catch (error) {
1485
- this.logger.warn(`Failed to publish metadata event`, { type, name, error });
1486
- }
1487
- }
1683
+ await this.publishRealtimeMetadataEvent(existed ? "updated" : "created", type, name, {
1684
+ definition: data,
1685
+ packageId: data?.packageId,
1686
+ userId: options?.userId
1687
+ });
1488
1688
  if (options?.notify !== false) {
1489
1689
  this.notifyWatchers(type, {
1490
1690
  type: existed ? "changed" : "added",
@@ -1598,23 +1798,9 @@ var _MetadataManager = class _MetadataManager {
1598
1798
  }
1599
1799
  }
1600
1800
  }
1601
- if (this.realtimeService) {
1602
- const event = {
1603
- type: `metadata.${type}.deleted`,
1604
- object: type,
1605
- payload: {
1606
- metadataType: type,
1607
- name
1608
- },
1609
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1610
- };
1611
- try {
1612
- await this.realtimeService.publish(event);
1613
- this.logger.debug(`Published metadata.${type}.deleted event`, { name });
1614
- } catch (error) {
1615
- this.logger.warn(`Failed to publish metadata event`, { type, name, error });
1616
- }
1617
- }
1801
+ await this.publishRealtimeMetadataEvent("deleted", type, name, {
1802
+ userId: options?.userId
1803
+ });
1618
1804
  if (options?.notify !== false) {
1619
1805
  this.notifyWatchers(type, {
1620
1806
  type: "deleted",
@@ -3315,6 +3501,14 @@ var MetadataPlugin = class {
3315
3501
  * initializes.
3316
3502
  */
3317
3503
  this.providesServices = ["metadata"];
3504
+ /**
3505
+ * init() registers the metadata system objects through the `manifest`
3506
+ * service ObjectQLPlugin provides — order-if-present so that
3507
+ * registration is deterministic instead of "whichever init ran first"
3508
+ * (ADR-0116, #4471). Soft, not hard: without an engine the plugin
3509
+ * degrades on purpose (objects are discovered via the legacy fallback).
3510
+ */
3511
+ this.optionalDependencies = ["com.objectstack.engine.objectql"];
3318
3512
  this.init = async (ctx) => {
3319
3513
  ctx.logger.info("Initializing Metadata Manager", {
3320
3514
  root: this.options.rootDir || process.cwd(),