@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 CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ServiceObject, ObjectOwnership, HookContext, QueryAST, EngineQueryOptions, DataEngineInsertOptions, EngineUpdateOptions, EngineDeleteOptions, EngineCountOptions, EngineAggregateOptions, DateGranularityValue, Hook } from '@objectstack/spec/data';
2
2
  import { ObjectStackManifest, InstalledPackage, ExecutionContext } from '@objectstack/spec/kernel';
3
+ import * as _objectstack_metadata_core from '@objectstack/metadata-core';
4
+ import { MetadataRepository, MetaRef, MetadataItem, PutOptions, PutResult, DeleteOptions, DeleteResult, ListFilter, MetadataItemHeader, HistoryOptions, MetadataEvent, WatchFilter } from '@objectstack/metadata-core';
3
5
  import { ObjectStackProtocol, MetadataCacheRequest, MetadataCacheResponse, BatchUpdateRequest, BatchUpdateResponse, UpdateManyDataRequest, DeleteManyDataRequest } from '@objectstack/spec/api';
4
6
  import { IDataEngine, DriverInterface, Logger, Plugin, PluginContext, ObjectKernel } from '@objectstack/core';
5
7
  import { IFeedService, IRealtimeService } from '@objectstack/spec/contracts';
@@ -244,6 +246,21 @@ declare class SchemaRegistry {
244
246
  id: string;
245
247
  globs: string[];
246
248
  }[];
249
+ /**
250
+ * Invalidate the merged-schema cache for a single FQN (or short name).
251
+ *
252
+ * Call this from event-driven consumers (ADR-0008 M0 PR-7) when an
253
+ * upstream metadata change makes the cached merged definition stale.
254
+ * The contributor list is preserved — only the cached merge result is
255
+ * dropped, so the next `resolveObject(fqn)` recomputes from scratch.
256
+ *
257
+ * Accepts either an FQN (`acme__contact`) or a bare short name
258
+ * (`contact`); for the latter, all entries whose suffix matches the
259
+ * name are invalidated.
260
+ */
261
+ invalidate(fqnOrName: string): void;
262
+ /** Drop every entry from the merged-schema cache. */
263
+ invalidateAll(): void;
247
264
  /**
248
265
  * Clear all registry state. Use only for testing.
249
266
  */
@@ -263,7 +280,20 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
263
280
  * metadata even if several projects share the same physical database.
264
281
  */
265
282
  private projectId?;
283
+ /**
284
+ * Lazily-instantiated SysMetadataRepository per organization. Keyed by
285
+ * `${organizationId ?? '__env__'}`. Repositories are stateful — they
286
+ * carry the per-org `seqCounter` and watch subscribers — so we cache
287
+ * them rather than constructing one per call.
288
+ */
289
+ private overlayRepos;
266
290
  constructor(engine: IDataEngine, getServicesRegistry?: () => Map<string, any>, getFeedService?: () => IFeedService | undefined, projectId?: string);
291
+ /**
292
+ * Lazily obtain a SysMetadataRepository for the given organization.
293
+ * Env-wide overlays (organizationId == null) share a singleton under
294
+ * the `__env__` key.
295
+ */
296
+ private getOverlayRepo;
267
297
  /**
268
298
  * One-time guard for ensuring the overlay-uniqueness UNIQUE INDEX exists
269
299
  * on `sys_metadata`. ADR-0005: scopes overlays by
@@ -418,6 +448,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
418
448
  object: string;
419
449
  id: string;
420
450
  data: any;
451
+ expectedVersion?: string;
421
452
  context?: any;
422
453
  }): Promise<{
423
454
  object: string;
@@ -427,12 +458,33 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
427
458
  deleteData(request: {
428
459
  object: string;
429
460
  id: string;
461
+ expectedVersion?: string;
430
462
  context?: any;
431
463
  }): Promise<{
432
464
  object: string;
433
465
  id: string;
434
466
  success: boolean;
435
467
  }>;
468
+ /**
469
+ * Optimistic Concurrency Control gate shared by updateData/deleteData.
470
+ *
471
+ * When the caller passes a non-empty `expectedVersion` token (typically
472
+ * the `updated_at` value they read), this fetches the current record
473
+ * and compares its `updated_at` against the token. Mismatch → throw
474
+ * `ConcurrentUpdateError` which the REST layer maps to 409.
475
+ *
476
+ * Behaviour:
477
+ * - Empty/missing token → no check (opt-in semantics; existing callers
478
+ * that haven't yet adopted OCC are unaffected).
479
+ * - Record not found → no check; downstream `engine.update` will
480
+ * surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
481
+ * treat "missing record" as a concurrency conflict.
482
+ * - Record has no `updated_at` field (timestamps disabled) → no check.
483
+ * Logging would be noisy here; OCC is opt-in and the absence of a
484
+ * version column is an explicit "this object doesn't support OCC"
485
+ * signal.
486
+ */
487
+ private assertVersionMatch;
436
488
  /**
437
489
  * Cross-object substring search across all registered objects that opt in
438
490
  * via `enable.searchable !== false` and `enable.apiEnabled !== false`.
@@ -539,14 +591,53 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
539
591
  private static readonly OVERLAY_ALLOWED_TYPES;
540
592
  /** Normalize plural→singular before consulting the allow-list. */
541
593
  private static isOverlayAllowed;
594
+ /**
595
+ * Mirror an object-type overlay write into the in-memory engine
596
+ * registry so subsequent CRUD finds the new schema. Idempotent and
597
+ * safe to call after a successful persistence call. For the legacy
598
+ * write path this is invoked BEFORE persistence (historical behavior
599
+ * preserved); for the PR-10d.3 repository path it is invoked only
600
+ * AFTER `put()` resolves successfully, so a failed write — DB error,
601
+ * optimistic-lock conflict, validation failure — never leaks a
602
+ * stale schema into the registry.
603
+ */
604
+ private applyObjectRegistryMutation;
542
605
  saveMetaItem(request: {
543
606
  type: string;
544
607
  name: string;
545
608
  item?: any;
546
609
  organizationId?: string;
610
+ parentVersion?: string | null;
611
+ actor?: string;
547
612
  }): Promise<{
548
613
  success: boolean;
614
+ version: string;
615
+ seq: number;
549
616
  message: string;
617
+ } | {
618
+ success: boolean;
619
+ message: string;
620
+ version?: undefined;
621
+ seq?: undefined;
622
+ }>;
623
+ /**
624
+ * Yield the durable change-log for a single metadata item — every
625
+ * put/delete recorded in `sys_metadata_history` for `(org, type, name)`,
626
+ * in event_seq order. Powers the Studio "History" tab and any
627
+ * client-side audit timeline.
628
+ *
629
+ * Returns `[]` for non-overlay-allowed types (the legacy raw-engine
630
+ * path doesn't record history) instead of throwing — callers can treat
631
+ * "no history" uniformly.
632
+ */
633
+ historyMetaItem(request: {
634
+ type: string;
635
+ name: string;
636
+ organizationId?: string;
637
+ sinceSeq?: number;
638
+ limit?: number;
639
+ }): Promise<{
640
+ events: _objectstack_metadata_core.MetadataEvent[];
550
641
  }>;
551
642
  /**
552
643
  * Remove a customization overlay row for the given metadata item, so the
@@ -558,10 +649,13 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
558
649
  type: string;
559
650
  name: string;
560
651
  organizationId?: string;
652
+ parentVersion?: string | null;
653
+ actor?: string;
561
654
  }): Promise<{
562
655
  success: boolean;
563
656
  message?: string;
564
657
  reset?: boolean;
658
+ seq?: number;
565
659
  }>;
566
660
  /**
567
661
  * Hydrate SchemaRegistry from the database on startup.
@@ -592,6 +686,132 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
592
686
  feedUnsubscribe(request: any): Promise<any>;
593
687
  }
594
688
 
689
+ /**
690
+ * Sub-set of the ObjectQL engine shape we depend on. Kept narrow so
691
+ * tests can stub it with a plain mock. Mirrors the real engine's
692
+ * `options.context` pattern so transactions can thread through.
693
+ */
694
+ interface SysMetadataEngine {
695
+ find(table: string, options: {
696
+ where: Record<string, unknown>;
697
+ limit?: number;
698
+ orderBy?: any;
699
+ context?: any;
700
+ }): Promise<any[]>;
701
+ findOne(table: string, options: {
702
+ where: Record<string, unknown>;
703
+ context?: any;
704
+ }): Promise<any | null>;
705
+ insert(table: string, data: Record<string, unknown>, options?: {
706
+ context?: any;
707
+ }): Promise<{
708
+ id: string;
709
+ }>;
710
+ update(table: string, data: Record<string, unknown>, options: {
711
+ where: Record<string, unknown>;
712
+ context?: any;
713
+ }): Promise<{
714
+ id: string;
715
+ }>;
716
+ delete(table: string, options: {
717
+ where: Record<string, unknown>;
718
+ context?: any;
719
+ }): Promise<{
720
+ deleted: number;
721
+ }>;
722
+ /**
723
+ * Optional. Falls through to direct callback invocation if the
724
+ * underlying driver lacks ACID support (matches the real
725
+ * `ObjectQL.transaction` semantics). Repository code must not rely on
726
+ * rollback for correctness against in-memory drivers.
727
+ */
728
+ transaction?<T>(callback: (trxCtx: any) => Promise<T>, baseContext?: any): Promise<T>;
729
+ }
730
+ interface SysMetadataRepositoryOptions {
731
+ engine: SysMetadataEngine;
732
+ /**
733
+ * Tenancy scope. `null` writes to env-wide overlay rows; a string
734
+ * scopes to one organization (the supported shared-DB tenant model
735
+ * — see ADR-0005 amendment).
736
+ */
737
+ organizationId?: string | null;
738
+ /** Org label embedded in returned MetaRefs. Defaults to organizationId or `"system"`. */
739
+ orgLabel?: string;
740
+ }
741
+ declare class SysMetadataRepository implements MetadataRepository {
742
+ private readonly engine;
743
+ private readonly organizationId;
744
+ private readonly orgLabel;
745
+ /**
746
+ * Local seq counter for in-memory watch() event broadcasts. Mirrors
747
+ * the durable `event_seq` we write into `sys_metadata_history` on
748
+ * each successful put/delete — assigned AFTER the transaction commits
749
+ * so we never broadcast events that got rolled back.
750
+ */
751
+ private seqCounter;
752
+ private readonly watchers;
753
+ private closed;
754
+ /** Table name for the durable event log. */
755
+ private readonly historyTable;
756
+ constructor(opts: SysMetadataRepositoryOptions);
757
+ /**
758
+ * Run `cb` inside `engine.transaction(...)` if the engine supports it,
759
+ * otherwise fall through to a direct call. Matches the real
760
+ * `ObjectQL.transaction` semantics — in-memory drivers (and our test
761
+ * fakes) get no rollback, which is acceptable because production
762
+ * always runs on a SQL driver with real ACID.
763
+ */
764
+ private withTxn;
765
+ /**
766
+ * Read the current overlay row. Returns null if no row exists —
767
+ * callers (e.g. LayeredRepository) fall through to lower layers.
768
+ */
769
+ get(ref: MetaRef): Promise<MetadataItem | null>;
770
+ put(ref: MetaRef, spec: unknown, opts: PutOptions): Promise<PutResult>;
771
+ delete(ref: MetaRef, opts: DeleteOptions): Promise<DeleteResult>;
772
+ list(filter: ListFilter): AsyncIterable<MetadataItemHeader>;
773
+ /**
774
+ * Yield every history event for `(org, type?, name?)` from the
775
+ * durable log, ordered by per-(type,name) `version` ascending. When
776
+ * `filter.type`/`filter.name` are unset the consumer gets the full
777
+ * org-scoped event stream — still ordered by version within each
778
+ * (type,name) bucket, then by `recorded_at` across buckets (we sort
779
+ * client-side because the test engine doesn't honor `orderBy`).
780
+ */
781
+ history(ref: MetaRef, opts?: HistoryOptions): AsyncIterable<MetadataEvent>;
782
+ /**
783
+ * Live event stream. Fires for every successful put/delete on THIS
784
+ * instance — cross-replica fan-out is M1. Manual AsyncIterator (not
785
+ * an async generator) so we can deterministically tear down via
786
+ * `iter.return()`, matching the pattern used by InMemoryRepository.
787
+ */
788
+ watch(filter: WatchFilter, since?: number): AsyncIterable<MetadataEvent>;
789
+ /** Shut down all watch iterators. */
790
+ close(): void;
791
+ private assertOpen;
792
+ private assertAllowed;
793
+ private whereFor;
794
+ private fullRef;
795
+ private rowToItem;
796
+ private broadcast;
797
+ private matchesFilter;
798
+ /**
799
+ * Per-org monotonic event sequence. Reads `MAX(event_seq) + 1` from
800
+ * `sys_metadata_history` scoped by `organization_id`. MUST be called
801
+ * inside a transaction (the only caller is the put/delete txn body) —
802
+ * concurrent writers in the same org race otherwise.
803
+ */
804
+ private nextEventSeq;
805
+ /**
806
+ * Per-(org,type,name) lineage counter. Reads from history (not from
807
+ * `sys_metadata.version`) so delete + recreate continues incrementing
808
+ * instead of restarting at 1.
809
+ */
810
+ private nextItemVersion;
811
+ /** Lightweight UUID-ish id for history rows; sufficient for an audit log. */
812
+ private uuid;
813
+ }
814
+
595
815
  type HookHandler = (context: HookContext) => Promise<void> | void;
596
816
  /**
597
817
  * Per-object hook entry with priority support
@@ -1493,9 +1713,25 @@ declare class ObjectQLPlugin implements Plugin {
1493
1713
  private hostContext?;
1494
1714
  private projectId?;
1495
1715
  private skipSchemaSync;
1716
+ /** Unsubscribe handles for metadata-event subscriptions (ADR-0008 PR-7). */
1717
+ private metadataUnsubscribes;
1496
1718
  constructor(qlOrOptions?: ObjectQL | ObjectQLPluginOptions, hostContext?: Record<string, any>);
1497
1719
  init: (ctx: PluginContext) => Promise<void>;
1498
1720
  start: (ctx: PluginContext) => Promise<void>;
1721
+ stop: (ctx: PluginContext) => Promise<void>;
1722
+ /**
1723
+ * Subscribe to `object` metadata events from the metadata service and
1724
+ * invalidate the SchemaRegistry merge cache on each event (ADR-0008
1725
+ * PR-7). For create/update we also re-load the affected object from
1726
+ * the metadata service so subsequent reads see the new definition;
1727
+ * for delete we unregister it from every contributing package.
1728
+ *
1729
+ * Events are filtered to the canonical `object` type — view/dashboard
1730
+ * /flow edits go through their own consumers (Studio SSE, REST cache).
1731
+ *
1732
+ * Stored unsubscribe handle is invoked from {@link stop}.
1733
+ */
1734
+ private subscribeToMetadataEvents;
1499
1735
  /**
1500
1736
  * Register built-in audit hooks for auto-stamping created_by/updated_by
1501
1737
  * and fetching previousData for update/delete operations. These are
@@ -1676,4 +1912,4 @@ declare function convertIntrospectedSchemaToObjects(introspectedSchema: Introspe
1676
1912
  skipSystemColumns?: boolean;
1677
1913
  }): ServiceObject[];
1678
1914
 
1679
- export { type BindHooksOptions, type BindHooksResult, DEFAULT_EXTENDER_PRIORITY, DEFAULT_OWNER_PRIORITY, type EngineMiddleware, type FieldValidationError, type HookEntry, type HookHandler, type HookMetricLabel, type HookMetricOutcome, type HookMetricsRecorder, type HookSkipReason, InMemoryHookMetricsRecorder, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, MetadataFacade, type ObjectContributor, ObjectQL, type ObjectQLHostContext, type ObjectQLKernelOptions, ObjectQLPlugin, ObjectRepository, ObjectStackProtocolImplementation, type OperationContext, RESERVED_NAMESPACES, SchemaRegistry, type SchemaRegistryOptions, ScopedContext, ValidationError, type WrapDeclarativeOptions, applyInMemoryAggregation, applySystemFields, bindHooksToEngine, bucketDateValue, computeFQN, convertIntrospectedSchemaToObjects, createObjectQLKernel, noopHookMetricsRecorder, parseFQN, toTitleCase, validateRecord, wrapDeclarativeHook };
1915
+ export { type BindHooksOptions, type BindHooksResult, DEFAULT_EXTENDER_PRIORITY, DEFAULT_OWNER_PRIORITY, type EngineMiddleware, type FieldValidationError, type HookEntry, type HookHandler, type HookMetricLabel, type HookMetricOutcome, type HookMetricsRecorder, type HookSkipReason, InMemoryHookMetricsRecorder, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, MetadataFacade, type ObjectContributor, ObjectQL, type ObjectQLHostContext, type ObjectQLKernelOptions, ObjectQLPlugin, ObjectRepository, ObjectStackProtocolImplementation, type OperationContext, RESERVED_NAMESPACES, SchemaRegistry, type SchemaRegistryOptions, ScopedContext, type SysMetadataEngine, SysMetadataRepository, type SysMetadataRepositoryOptions, ValidationError, type WrapDeclarativeOptions, applyInMemoryAggregation, applySystemFields, bindHooksToEngine, bucketDateValue, computeFQN, convertIntrospectedSchemaToObjects, createObjectQLKernel, noopHookMetricsRecorder, parseFQN, toTitleCase, validateRecord, wrapDeclarativeHook };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { ServiceObject, ObjectOwnership, HookContext, QueryAST, EngineQueryOptions, DataEngineInsertOptions, EngineUpdateOptions, EngineDeleteOptions, EngineCountOptions, EngineAggregateOptions, DateGranularityValue, Hook } from '@objectstack/spec/data';
2
2
  import { ObjectStackManifest, InstalledPackage, ExecutionContext } from '@objectstack/spec/kernel';
3
+ import * as _objectstack_metadata_core from '@objectstack/metadata-core';
4
+ import { MetadataRepository, MetaRef, MetadataItem, PutOptions, PutResult, DeleteOptions, DeleteResult, ListFilter, MetadataItemHeader, HistoryOptions, MetadataEvent, WatchFilter } from '@objectstack/metadata-core';
3
5
  import { ObjectStackProtocol, MetadataCacheRequest, MetadataCacheResponse, BatchUpdateRequest, BatchUpdateResponse, UpdateManyDataRequest, DeleteManyDataRequest } from '@objectstack/spec/api';
4
6
  import { IDataEngine, DriverInterface, Logger, Plugin, PluginContext, ObjectKernel } from '@objectstack/core';
5
7
  import { IFeedService, IRealtimeService } from '@objectstack/spec/contracts';
@@ -244,6 +246,21 @@ declare class SchemaRegistry {
244
246
  id: string;
245
247
  globs: string[];
246
248
  }[];
249
+ /**
250
+ * Invalidate the merged-schema cache for a single FQN (or short name).
251
+ *
252
+ * Call this from event-driven consumers (ADR-0008 M0 PR-7) when an
253
+ * upstream metadata change makes the cached merged definition stale.
254
+ * The contributor list is preserved — only the cached merge result is
255
+ * dropped, so the next `resolveObject(fqn)` recomputes from scratch.
256
+ *
257
+ * Accepts either an FQN (`acme__contact`) or a bare short name
258
+ * (`contact`); for the latter, all entries whose suffix matches the
259
+ * name are invalidated.
260
+ */
261
+ invalidate(fqnOrName: string): void;
262
+ /** Drop every entry from the merged-schema cache. */
263
+ invalidateAll(): void;
247
264
  /**
248
265
  * Clear all registry state. Use only for testing.
249
266
  */
@@ -263,7 +280,20 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
263
280
  * metadata even if several projects share the same physical database.
264
281
  */
265
282
  private projectId?;
283
+ /**
284
+ * Lazily-instantiated SysMetadataRepository per organization. Keyed by
285
+ * `${organizationId ?? '__env__'}`. Repositories are stateful — they
286
+ * carry the per-org `seqCounter` and watch subscribers — so we cache
287
+ * them rather than constructing one per call.
288
+ */
289
+ private overlayRepos;
266
290
  constructor(engine: IDataEngine, getServicesRegistry?: () => Map<string, any>, getFeedService?: () => IFeedService | undefined, projectId?: string);
291
+ /**
292
+ * Lazily obtain a SysMetadataRepository for the given organization.
293
+ * Env-wide overlays (organizationId == null) share a singleton under
294
+ * the `__env__` key.
295
+ */
296
+ private getOverlayRepo;
267
297
  /**
268
298
  * One-time guard for ensuring the overlay-uniqueness UNIQUE INDEX exists
269
299
  * on `sys_metadata`. ADR-0005: scopes overlays by
@@ -418,6 +448,7 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
418
448
  object: string;
419
449
  id: string;
420
450
  data: any;
451
+ expectedVersion?: string;
421
452
  context?: any;
422
453
  }): Promise<{
423
454
  object: string;
@@ -427,12 +458,33 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
427
458
  deleteData(request: {
428
459
  object: string;
429
460
  id: string;
461
+ expectedVersion?: string;
430
462
  context?: any;
431
463
  }): Promise<{
432
464
  object: string;
433
465
  id: string;
434
466
  success: boolean;
435
467
  }>;
468
+ /**
469
+ * Optimistic Concurrency Control gate shared by updateData/deleteData.
470
+ *
471
+ * When the caller passes a non-empty `expectedVersion` token (typically
472
+ * the `updated_at` value they read), this fetches the current record
473
+ * and compares its `updated_at` against the token. Mismatch → throw
474
+ * `ConcurrentUpdateError` which the REST layer maps to 409.
475
+ *
476
+ * Behaviour:
477
+ * - Empty/missing token → no check (opt-in semantics; existing callers
478
+ * that haven't yet adopted OCC are unaffected).
479
+ * - Record not found → no check; downstream `engine.update` will
480
+ * surface the usual `RECORD_NOT_FOUND` 404. We intentionally do not
481
+ * treat "missing record" as a concurrency conflict.
482
+ * - Record has no `updated_at` field (timestamps disabled) → no check.
483
+ * Logging would be noisy here; OCC is opt-in and the absence of a
484
+ * version column is an explicit "this object doesn't support OCC"
485
+ * signal.
486
+ */
487
+ private assertVersionMatch;
436
488
  /**
437
489
  * Cross-object substring search across all registered objects that opt in
438
490
  * via `enable.searchable !== false` and `enable.apiEnabled !== false`.
@@ -539,14 +591,53 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
539
591
  private static readonly OVERLAY_ALLOWED_TYPES;
540
592
  /** Normalize plural→singular before consulting the allow-list. */
541
593
  private static isOverlayAllowed;
594
+ /**
595
+ * Mirror an object-type overlay write into the in-memory engine
596
+ * registry so subsequent CRUD finds the new schema. Idempotent and
597
+ * safe to call after a successful persistence call. For the legacy
598
+ * write path this is invoked BEFORE persistence (historical behavior
599
+ * preserved); for the PR-10d.3 repository path it is invoked only
600
+ * AFTER `put()` resolves successfully, so a failed write — DB error,
601
+ * optimistic-lock conflict, validation failure — never leaks a
602
+ * stale schema into the registry.
603
+ */
604
+ private applyObjectRegistryMutation;
542
605
  saveMetaItem(request: {
543
606
  type: string;
544
607
  name: string;
545
608
  item?: any;
546
609
  organizationId?: string;
610
+ parentVersion?: string | null;
611
+ actor?: string;
547
612
  }): Promise<{
548
613
  success: boolean;
614
+ version: string;
615
+ seq: number;
549
616
  message: string;
617
+ } | {
618
+ success: boolean;
619
+ message: string;
620
+ version?: undefined;
621
+ seq?: undefined;
622
+ }>;
623
+ /**
624
+ * Yield the durable change-log for a single metadata item — every
625
+ * put/delete recorded in `sys_metadata_history` for `(org, type, name)`,
626
+ * in event_seq order. Powers the Studio "History" tab and any
627
+ * client-side audit timeline.
628
+ *
629
+ * Returns `[]` for non-overlay-allowed types (the legacy raw-engine
630
+ * path doesn't record history) instead of throwing — callers can treat
631
+ * "no history" uniformly.
632
+ */
633
+ historyMetaItem(request: {
634
+ type: string;
635
+ name: string;
636
+ organizationId?: string;
637
+ sinceSeq?: number;
638
+ limit?: number;
639
+ }): Promise<{
640
+ events: _objectstack_metadata_core.MetadataEvent[];
550
641
  }>;
551
642
  /**
552
643
  * Remove a customization overlay row for the given metadata item, so the
@@ -558,10 +649,13 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
558
649
  type: string;
559
650
  name: string;
560
651
  organizationId?: string;
652
+ parentVersion?: string | null;
653
+ actor?: string;
561
654
  }): Promise<{
562
655
  success: boolean;
563
656
  message?: string;
564
657
  reset?: boolean;
658
+ seq?: number;
565
659
  }>;
566
660
  /**
567
661
  * Hydrate SchemaRegistry from the database on startup.
@@ -592,6 +686,132 @@ declare class ObjectStackProtocolImplementation implements ObjectStackProtocol {
592
686
  feedUnsubscribe(request: any): Promise<any>;
593
687
  }
594
688
 
689
+ /**
690
+ * Sub-set of the ObjectQL engine shape we depend on. Kept narrow so
691
+ * tests can stub it with a plain mock. Mirrors the real engine's
692
+ * `options.context` pattern so transactions can thread through.
693
+ */
694
+ interface SysMetadataEngine {
695
+ find(table: string, options: {
696
+ where: Record<string, unknown>;
697
+ limit?: number;
698
+ orderBy?: any;
699
+ context?: any;
700
+ }): Promise<any[]>;
701
+ findOne(table: string, options: {
702
+ where: Record<string, unknown>;
703
+ context?: any;
704
+ }): Promise<any | null>;
705
+ insert(table: string, data: Record<string, unknown>, options?: {
706
+ context?: any;
707
+ }): Promise<{
708
+ id: string;
709
+ }>;
710
+ update(table: string, data: Record<string, unknown>, options: {
711
+ where: Record<string, unknown>;
712
+ context?: any;
713
+ }): Promise<{
714
+ id: string;
715
+ }>;
716
+ delete(table: string, options: {
717
+ where: Record<string, unknown>;
718
+ context?: any;
719
+ }): Promise<{
720
+ deleted: number;
721
+ }>;
722
+ /**
723
+ * Optional. Falls through to direct callback invocation if the
724
+ * underlying driver lacks ACID support (matches the real
725
+ * `ObjectQL.transaction` semantics). Repository code must not rely on
726
+ * rollback for correctness against in-memory drivers.
727
+ */
728
+ transaction?<T>(callback: (trxCtx: any) => Promise<T>, baseContext?: any): Promise<T>;
729
+ }
730
+ interface SysMetadataRepositoryOptions {
731
+ engine: SysMetadataEngine;
732
+ /**
733
+ * Tenancy scope. `null` writes to env-wide overlay rows; a string
734
+ * scopes to one organization (the supported shared-DB tenant model
735
+ * — see ADR-0005 amendment).
736
+ */
737
+ organizationId?: string | null;
738
+ /** Org label embedded in returned MetaRefs. Defaults to organizationId or `"system"`. */
739
+ orgLabel?: string;
740
+ }
741
+ declare class SysMetadataRepository implements MetadataRepository {
742
+ private readonly engine;
743
+ private readonly organizationId;
744
+ private readonly orgLabel;
745
+ /**
746
+ * Local seq counter for in-memory watch() event broadcasts. Mirrors
747
+ * the durable `event_seq` we write into `sys_metadata_history` on
748
+ * each successful put/delete — assigned AFTER the transaction commits
749
+ * so we never broadcast events that got rolled back.
750
+ */
751
+ private seqCounter;
752
+ private readonly watchers;
753
+ private closed;
754
+ /** Table name for the durable event log. */
755
+ private readonly historyTable;
756
+ constructor(opts: SysMetadataRepositoryOptions);
757
+ /**
758
+ * Run `cb` inside `engine.transaction(...)` if the engine supports it,
759
+ * otherwise fall through to a direct call. Matches the real
760
+ * `ObjectQL.transaction` semantics — in-memory drivers (and our test
761
+ * fakes) get no rollback, which is acceptable because production
762
+ * always runs on a SQL driver with real ACID.
763
+ */
764
+ private withTxn;
765
+ /**
766
+ * Read the current overlay row. Returns null if no row exists —
767
+ * callers (e.g. LayeredRepository) fall through to lower layers.
768
+ */
769
+ get(ref: MetaRef): Promise<MetadataItem | null>;
770
+ put(ref: MetaRef, spec: unknown, opts: PutOptions): Promise<PutResult>;
771
+ delete(ref: MetaRef, opts: DeleteOptions): Promise<DeleteResult>;
772
+ list(filter: ListFilter): AsyncIterable<MetadataItemHeader>;
773
+ /**
774
+ * Yield every history event for `(org, type?, name?)` from the
775
+ * durable log, ordered by per-(type,name) `version` ascending. When
776
+ * `filter.type`/`filter.name` are unset the consumer gets the full
777
+ * org-scoped event stream — still ordered by version within each
778
+ * (type,name) bucket, then by `recorded_at` across buckets (we sort
779
+ * client-side because the test engine doesn't honor `orderBy`).
780
+ */
781
+ history(ref: MetaRef, opts?: HistoryOptions): AsyncIterable<MetadataEvent>;
782
+ /**
783
+ * Live event stream. Fires for every successful put/delete on THIS
784
+ * instance — cross-replica fan-out is M1. Manual AsyncIterator (not
785
+ * an async generator) so we can deterministically tear down via
786
+ * `iter.return()`, matching the pattern used by InMemoryRepository.
787
+ */
788
+ watch(filter: WatchFilter, since?: number): AsyncIterable<MetadataEvent>;
789
+ /** Shut down all watch iterators. */
790
+ close(): void;
791
+ private assertOpen;
792
+ private assertAllowed;
793
+ private whereFor;
794
+ private fullRef;
795
+ private rowToItem;
796
+ private broadcast;
797
+ private matchesFilter;
798
+ /**
799
+ * Per-org monotonic event sequence. Reads `MAX(event_seq) + 1` from
800
+ * `sys_metadata_history` scoped by `organization_id`. MUST be called
801
+ * inside a transaction (the only caller is the put/delete txn body) —
802
+ * concurrent writers in the same org race otherwise.
803
+ */
804
+ private nextEventSeq;
805
+ /**
806
+ * Per-(org,type,name) lineage counter. Reads from history (not from
807
+ * `sys_metadata.version`) so delete + recreate continues incrementing
808
+ * instead of restarting at 1.
809
+ */
810
+ private nextItemVersion;
811
+ /** Lightweight UUID-ish id for history rows; sufficient for an audit log. */
812
+ private uuid;
813
+ }
814
+
595
815
  type HookHandler = (context: HookContext) => Promise<void> | void;
596
816
  /**
597
817
  * Per-object hook entry with priority support
@@ -1493,9 +1713,25 @@ declare class ObjectQLPlugin implements Plugin {
1493
1713
  private hostContext?;
1494
1714
  private projectId?;
1495
1715
  private skipSchemaSync;
1716
+ /** Unsubscribe handles for metadata-event subscriptions (ADR-0008 PR-7). */
1717
+ private metadataUnsubscribes;
1496
1718
  constructor(qlOrOptions?: ObjectQL | ObjectQLPluginOptions, hostContext?: Record<string, any>);
1497
1719
  init: (ctx: PluginContext) => Promise<void>;
1498
1720
  start: (ctx: PluginContext) => Promise<void>;
1721
+ stop: (ctx: PluginContext) => Promise<void>;
1722
+ /**
1723
+ * Subscribe to `object` metadata events from the metadata service and
1724
+ * invalidate the SchemaRegistry merge cache on each event (ADR-0008
1725
+ * PR-7). For create/update we also re-load the affected object from
1726
+ * the metadata service so subsequent reads see the new definition;
1727
+ * for delete we unregister it from every contributing package.
1728
+ *
1729
+ * Events are filtered to the canonical `object` type — view/dashboard
1730
+ * /flow edits go through their own consumers (Studio SSE, REST cache).
1731
+ *
1732
+ * Stored unsubscribe handle is invoked from {@link stop}.
1733
+ */
1734
+ private subscribeToMetadataEvents;
1499
1735
  /**
1500
1736
  * Register built-in audit hooks for auto-stamping created_by/updated_by
1501
1737
  * and fetching previousData for update/delete operations. These are
@@ -1676,4 +1912,4 @@ declare function convertIntrospectedSchemaToObjects(introspectedSchema: Introspe
1676
1912
  skipSystemColumns?: boolean;
1677
1913
  }): ServiceObject[];
1678
1914
 
1679
- export { type BindHooksOptions, type BindHooksResult, DEFAULT_EXTENDER_PRIORITY, DEFAULT_OWNER_PRIORITY, type EngineMiddleware, type FieldValidationError, type HookEntry, type HookHandler, type HookMetricLabel, type HookMetricOutcome, type HookMetricsRecorder, type HookSkipReason, InMemoryHookMetricsRecorder, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, MetadataFacade, type ObjectContributor, ObjectQL, type ObjectQLHostContext, type ObjectQLKernelOptions, ObjectQLPlugin, ObjectRepository, ObjectStackProtocolImplementation, type OperationContext, RESERVED_NAMESPACES, SchemaRegistry, type SchemaRegistryOptions, ScopedContext, ValidationError, type WrapDeclarativeOptions, applyInMemoryAggregation, applySystemFields, bindHooksToEngine, bucketDateValue, computeFQN, convertIntrospectedSchemaToObjects, createObjectQLKernel, noopHookMetricsRecorder, parseFQN, toTitleCase, validateRecord, wrapDeclarativeHook };
1915
+ export { type BindHooksOptions, type BindHooksResult, DEFAULT_EXTENDER_PRIORITY, DEFAULT_OWNER_PRIORITY, type EngineMiddleware, type FieldValidationError, type HookEntry, type HookHandler, type HookMetricLabel, type HookMetricOutcome, type HookMetricsRecorder, type HookSkipReason, InMemoryHookMetricsRecorder, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, MetadataFacade, type ObjectContributor, ObjectQL, type ObjectQLHostContext, type ObjectQLKernelOptions, ObjectQLPlugin, ObjectRepository, ObjectStackProtocolImplementation, type OperationContext, RESERVED_NAMESPACES, SchemaRegistry, type SchemaRegistryOptions, ScopedContext, type SysMetadataEngine, SysMetadataRepository, type SysMetadataRepositoryOptions, ValidationError, type WrapDeclarativeOptions, applyInMemoryAggregation, applySystemFields, bindHooksToEngine, bucketDateValue, computeFQN, convertIntrospectedSchemaToObjects, createObjectQLKernel, noopHookMetricsRecorder, parseFQN, toTitleCase, validateRecord, wrapDeclarativeHook };