@prisma-next/adapter-mongo 0.12.0 → 0.13.0-dev.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/package.json CHANGED
@@ -1,40 +1,42 @@
1
1
  {
2
2
  "name": "@prisma-next/adapter-mongo",
3
- "version": "0.12.0",
3
+ "version": "0.13.0-dev.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "description": "MongoDB adapter for Prisma Next (lowers commands to wire format)",
8
8
  "dependencies": {
9
- "@prisma-next/config": "0.12.0",
10
- "@prisma-next/contract": "0.12.0",
11
- "@prisma-next/errors": "0.12.0",
12
- "@prisma-next/family-mongo": "0.12.0",
13
- "@prisma-next/framework-components": "0.12.0",
14
- "@prisma-next/mongo-codec": "0.12.0",
15
- "@prisma-next/mongo-contract": "0.12.0",
16
- "@prisma-next/mongo-lowering": "0.12.0",
17
- "@prisma-next/mongo-query-ast": "0.12.0",
18
- "@prisma-next/mongo-schema-ir": "0.12.0",
19
- "@prisma-next/mongo-value": "0.12.0",
20
- "@prisma-next/mongo-wire": "0.12.0",
21
- "@prisma-next/operations": "0.12.0",
22
- "@prisma-next/utils": "0.12.0",
9
+ "@prisma-next/config": "0.13.0-dev.2",
10
+ "@prisma-next/contract": "0.13.0-dev.2",
11
+ "@prisma-next/errors": "0.13.0-dev.2",
12
+ "@prisma-next/family-mongo": "0.13.0-dev.2",
13
+ "@prisma-next/framework-components": "0.13.0-dev.2",
14
+ "@prisma-next/migration-tools": "0.13.0-dev.2",
15
+ "@prisma-next/mongo-codec": "0.13.0-dev.2",
16
+ "@prisma-next/mongo-contract": "0.13.0-dev.2",
17
+ "@prisma-next/mongo-lowering": "0.13.0-dev.2",
18
+ "@prisma-next/mongo-query-ast": "0.13.0-dev.2",
19
+ "@prisma-next/mongo-query-builder": "0.13.0-dev.2",
20
+ "@prisma-next/mongo-schema-ir": "0.13.0-dev.2",
21
+ "@prisma-next/mongo-value": "0.13.0-dev.2",
22
+ "@prisma-next/mongo-wire": "0.13.0-dev.2",
23
+ "@prisma-next/operations": "0.13.0-dev.2",
24
+ "@prisma-next/utils": "0.13.0-dev.2",
23
25
  "arktype": "^2.2.0"
24
26
  },
25
27
  "devDependencies": {
26
28
  "mongodb": "^7.2.0",
27
- "@prisma-next/driver-mongo": "0.12.0",
28
- "@prisma-next/errors": "0.12.0",
29
- "@prisma-next/mongo-contract-psl": "0.12.0",
30
- "@prisma-next/psl-parser": "0.12.0",
31
- "@prisma-next/test-utils": "0.12.0",
32
- "@prisma-next/tsconfig": "0.12.0",
33
- "@prisma-next/tsdown": "0.12.0",
34
- "mongodb-memory-server": "11.1.0",
35
- "tsdown": "0.22.0",
29
+ "@prisma-next/driver-mongo": "0.13.0-dev.2",
30
+ "@prisma-next/errors": "0.13.0-dev.2",
31
+ "@prisma-next/mongo-contract-psl": "0.13.0-dev.2",
32
+ "@prisma-next/psl-parser": "0.13.0-dev.2",
33
+ "@prisma-next/test-utils": "0.13.0-dev.2",
34
+ "@prisma-next/tsconfig": "0.13.0-dev.2",
35
+ "@prisma-next/tsdown": "0.13.0-dev.2",
36
+ "mongodb-memory-server": "11.2.0",
37
+ "tsdown": "0.22.1",
36
38
  "typescript": "5.9.3",
37
- "vitest": "4.1.6"
39
+ "vitest": "4.1.8"
38
40
  },
39
41
  "peerDependencies": {
40
42
  "mongodb": "^7.0.0",
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Document shape of the `_prisma_migrations` collection, which stores both the
3
+ * per-space contract **markers** (`space` / `storageHash` / … / `invariants`)
4
+ * and the migration **ledger** entries (`type` / `edgeId` / … / `appliedAt`) —
5
+ * so the shape is their union. Declared once so the contract-free field
6
+ * accessor is typed without threading a contract, codecs, or paths at the call
7
+ * sites.
8
+ */
9
+ export type MarkerLedgerDocShape = {
10
+ readonly _id: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
11
+ // Marker fields
12
+ readonly space: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
13
+ readonly storageHash: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
14
+ readonly profileHash: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
15
+ readonly contractJson: { readonly codecId: 'mongo/string@1'; readonly nullable: true };
16
+ readonly canonicalVersion: { readonly codecId: 'mongo/double@1'; readonly nullable: true };
17
+ readonly updatedAt: { readonly codecId: 'mongo/date@1'; readonly nullable: false };
18
+ readonly appTag: { readonly codecId: 'mongo/string@1'; readonly nullable: true };
19
+ readonly meta: { readonly codecId: 'mongo/document@1'; readonly nullable: true };
20
+ readonly invariants: { readonly codecId: 'mongo/array@1'; readonly nullable: false };
21
+ // Ledger fields
22
+ readonly type: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
23
+ readonly edgeId: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
24
+ readonly from: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
25
+ readonly to: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
26
+ readonly migrationName: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
27
+ readonly migrationHash: { readonly codecId: 'mongo/string@1'; readonly nullable: false };
28
+ readonly operations: { readonly codecId: 'mongo/array@1'; readonly nullable: false };
29
+ readonly appliedAt: { readonly codecId: 'mongo/date@1'; readonly nullable: false };
30
+ };
31
+
32
+ export const MARKER_LEDGER_COLLECTION = '_prisma_migrations';
@@ -1,27 +1,11 @@
1
1
  import type { ContractMarkerRecord } from '@prisma-next/contract/types';
2
- import { parseMarkerRowSafely, withMarkerReadErrorHandling } from '@prisma-next/errors/execution';
3
- import {
4
- RawAggregateCommand,
5
- RawFindOneAndUpdateCommand,
6
- RawInsertOneCommand,
7
- } from '@prisma-next/mongo-query-ast/execution';
2
+ import { parseMarkerRowSafely } from '@prisma-next/errors/execution';
8
3
  import { type } from 'arktype';
9
- import type { Db, Document, UpdateFilter } from 'mongodb';
10
4
 
11
- const COLLECTION = '_prisma_migrations';
12
- const MONGO_MARKER_COLLECTION = `_prisma_migrations marker documents in ${COLLECTION}`;
5
+ export const COLLECTION = '_prisma_migrations';
6
+ export const MONGO_MARKER_COLLECTION = `_prisma_migrations marker documents in ${COLLECTION}`;
7
+ export const MONGO_LEDGER_COLLECTION = `_prisma_migrations ledger documents in ${COLLECTION}`;
13
8
 
14
- /**
15
- * Marker doc shape.
16
- *
17
- * Same fields as the SQL marker row but camelCase + Mongo-native types:
18
- * `Date` is BSON-hydrated, `meta` is a native object (not JSON-stringified),
19
- * `_id` and any extension fields are tolerated. `invariants?` is optional —
20
- * absent reads as `[]` (schemaless default); present-but-malformed throws.
21
- *
22
- * `space` is required: every marker doc is keyed by its space id (`_id`)
23
- * and stamped with a matching `space` field for partitioned reads.
24
- */
25
9
  const MongoMarkerDocSchema = type({
26
10
  space: 'string',
27
11
  storageHash: 'string',
@@ -35,7 +19,7 @@ const MongoMarkerDocSchema = type({
35
19
  '+': 'delete',
36
20
  });
37
21
 
38
- function parseMongoMarkerDoc(doc: unknown): ContractMarkerRecord {
22
+ export function parseMongoMarkerDoc(doc: unknown): ContractMarkerRecord {
39
23
  const result = MongoMarkerDocSchema(doc);
40
24
  if (result instanceof type.errors) {
41
25
  throw new Error(`Invalid marker doc on ${COLLECTION}: ${result.summary}`);
@@ -47,206 +31,14 @@ function parseMongoMarkerDoc(doc: unknown): ContractMarkerRecord {
47
31
  canonicalVersion: result.canonicalVersion ?? null,
48
32
  updatedAt: result.updatedAt ?? new Date(),
49
33
  appTag: result.appTag ?? null,
50
- meta: (result.meta as Record<string, unknown> | null) ?? {},
34
+ meta: result.meta ?? {},
51
35
  invariants: result.invariants ?? [],
52
36
  };
53
37
  }
54
38
 
55
- function parseMongoMarkerDocSafely(doc: unknown, space: string): ContractMarkerRecord {
39
+ export function parseMongoMarkerDocSafely(doc: unknown, space: string): ContractMarkerRecord {
56
40
  return parseMarkerRowSafely(doc, parseMongoMarkerDoc, {
57
41
  space,
58
42
  markerLocation: MONGO_MARKER_COLLECTION,
59
43
  });
60
44
  }
61
-
62
- async function executeAggregate(db: Db, cmd: RawAggregateCommand): Promise<Document[]> {
63
- return db
64
- .collection(cmd.collection)
65
- .aggregate(cmd.pipeline as Record<string, unknown>[])
66
- .toArray();
67
- }
68
-
69
- async function executeInsertOne(db: Db, cmd: RawInsertOneCommand): Promise<void> {
70
- await db.collection(cmd.collection).insertOne(cmd.document);
71
- }
72
-
73
- async function executeFindOneAndUpdate(
74
- db: Db,
75
- cmd: RawFindOneAndUpdateCommand,
76
- ): Promise<Document | null> {
77
- // `cmd.update` is `Document | ReadonlyArray<Document>` per the AST. The
78
- // MongoDB driver's `findOneAndUpdate` accepts the same shape under the
79
- // type `UpdateFilter<T> | Document[]`. The driver's runtime path handles
80
- // both forms identically — pipelines (array) and update docs (object).
81
- // One cast to that union keeps the call single-arm.
82
- return db
83
- .collection(cmd.collection)
84
- .findOneAndUpdate(cmd.filter, cmd.update as UpdateFilter<Document> | Document[], {
85
- upsert: cmd.upsert,
86
- });
87
- }
88
-
89
- /**
90
- * Reads the marker document for the given contract space, or returns
91
- * `null` if no marker has been written for that space yet. Each space
92
- * owns one row keyed by `_id: <space>` — see ADR 212 for the per-space
93
- * mechanism this enables.
94
- */
95
- export async function readMarker(db: Db, space: string): Promise<ContractMarkerRecord | null> {
96
- const markerContext = { space, markerLocation: MONGO_MARKER_COLLECTION };
97
- const docs = await withMarkerReadErrorHandling(
98
- () =>
99
- executeAggregate(
100
- db,
101
- new RawAggregateCommand(COLLECTION, [{ $match: { _id: space, space } }, { $limit: 1 }]),
102
- ),
103
- markerContext,
104
- );
105
- const doc = docs[0];
106
- if (!doc) return null;
107
- return parseMongoMarkerDocSafely(doc, space);
108
- }
109
-
110
- /**
111
- * Reads every marker doc in the collection (one per contract space)
112
- * and returns them keyed by `space`. Used by the per-space verifier
113
- * to detect marker-vs-on-disk drift and orphan marker rows. Returns
114
- * an empty map when no marker docs have been written yet.
115
- *
116
- * Marker docs are keyed by `_id: <space>` (string); ledger entries
117
- * live in the same collection but use a driver-generated `ObjectId`
118
- * `_id` plus `type: 'ledger'`. The filter selects string-keyed docs
119
- * with a `space` field, which excludes ledger entries by construction.
120
- */
121
- export async function readAllMarkers(db: Db): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
122
- const markerContext = { space: 'app', markerLocation: MONGO_MARKER_COLLECTION };
123
- const docs = await withMarkerReadErrorHandling(
124
- () =>
125
- executeAggregate(
126
- db,
127
- new RawAggregateCommand(COLLECTION, [
128
- {
129
- $match: {
130
- _id: { $type: 'string' },
131
- space: { $type: 'string' },
132
- $expr: { $eq: ['$_id', '$space'] },
133
- },
134
- },
135
- ]),
136
- ),
137
- markerContext,
138
- );
139
- const out = new Map<string, ContractMarkerRecord>();
140
- for (const doc of docs) {
141
- const space = doc['space'];
142
- /* v8 ignore next -- @preserve type-narrowing guard: the $match stage above filters on `space: { $type: 'string' }`, so this branch is unreachable at runtime. The check exists so the `out.set(space, ...)` call below can accept `string`. */
143
- if (typeof space !== 'string') continue;
144
- out.set(space, parseMongoMarkerDocSafely(doc, space));
145
- }
146
- return out;
147
- }
148
-
149
- export async function initMarker(
150
- db: Db,
151
- space: string,
152
- destination: {
153
- readonly storageHash: string;
154
- readonly profileHash: string;
155
- readonly invariants?: readonly string[];
156
- },
157
- ): Promise<void> {
158
- const cmd = new RawInsertOneCommand(COLLECTION, {
159
- _id: space,
160
- space,
161
- storageHash: destination.storageHash,
162
- profileHash: destination.profileHash,
163
- contractJson: null,
164
- canonicalVersion: null,
165
- updatedAt: new Date(),
166
- appTag: null,
167
- meta: {},
168
- invariants: destination.invariants ?? [],
169
- });
170
- await executeInsertOne(db, cmd);
171
- }
172
-
173
- /**
174
- * Updates the marker doc for the given space atomically (CAS on
175
- * `expectedFrom`).
176
- *
177
- * `destination.invariants`:
178
- * - `undefined` → existing field left untouched.
179
- * - explicit value → merged into the existing field server-side via an
180
- * aggregation pipeline (`$setUnion + $sortArray`), atomic at the
181
- * document level. `[]` is a no-op merge.
182
- */
183
- export async function updateMarker(
184
- db: Db,
185
- space: string,
186
- expectedFrom: string,
187
- destination: {
188
- readonly storageHash: string;
189
- readonly profileHash: string;
190
- readonly invariants?: readonly string[];
191
- },
192
- ): Promise<boolean> {
193
- const setBase: Record<string, unknown> = {
194
- storageHash: destination.storageHash,
195
- profileHash: destination.profileHash,
196
- updatedAt: new Date(),
197
- };
198
- // When invariants is supplied, use an aggregation pipeline so the
199
- // merge runs server-side against the doc's current value (atomic, no
200
- // read-then-write window). When omitted, a regular update doc keeps
201
- // the field untouched.
202
- const update: Document | Document[] =
203
- destination.invariants === undefined
204
- ? { $set: setBase }
205
- : [
206
- {
207
- $set: {
208
- ...setBase,
209
- invariants: {
210
- $sortArray: {
211
- input: { $setUnion: [{ $ifNull: ['$invariants', []] }, destination.invariants] },
212
- sortBy: 1,
213
- },
214
- },
215
- },
216
- },
217
- ];
218
- const cmd = new RawFindOneAndUpdateCommand(
219
- COLLECTION,
220
- { _id: space, space, storageHash: expectedFrom },
221
- update,
222
- false,
223
- );
224
- const result = await executeFindOneAndUpdate(db, cmd);
225
- return result !== null;
226
- }
227
-
228
- /**
229
- * Appends a ledger entry for the given space. Ledger entries co-exist
230
- * with marker docs in the same collection; marker docs use `_id: <space>`
231
- * (string), ledger entries use `type: 'ledger'` plus a driver-generated
232
- * ObjectId. Reads partition the two by filter shape.
233
- *
234
- * The same `edgeId` may legitimately recur across different spaces (e.g.
235
- * a synthetic ∅→head edge on first apply), so the ledger key is
236
- * `(space, edgeId)` — the doc carries `space` for partitioned reads.
237
- */
238
- export async function writeLedgerEntry(
239
- db: Db,
240
- space: string,
241
- entry: { readonly edgeId: string; readonly from: string; readonly to: string },
242
- ): Promise<void> {
243
- const cmd = new RawInsertOneCommand(COLLECTION, {
244
- type: 'ledger',
245
- space,
246
- edgeId: entry.edgeId,
247
- from: entry.from,
248
- to: entry.to,
249
- appliedAt: new Date(),
250
- });
251
- await executeInsertOne(db, cmd);
252
- }
@@ -1,38 +1,143 @@
1
- import type { ContractMarkerRecord } from '@prisma-next/contract/types';
1
+ import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
2
+ import { withMarkerReadErrorHandling } from '@prisma-next/errors/execution';
2
3
  import type { MongoControlAdapter } from '@prisma-next/family-mongo/control-adapter';
3
4
  import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
5
+ import { ledgerOriginFromStored } from '@prisma-next/migration-tools/ledger-origin';
6
+ import type { MongoAdapter } from '@prisma-next/mongo-lowering';
7
+ import {
8
+ type AnyMongoCommand,
9
+ MongoAggFieldRef,
10
+ MongoAggLiteral,
11
+ MongoAggOperator,
12
+ type MongoQueryPlan,
13
+ } from '@prisma-next/mongo-query-ast/execution';
14
+ import { expr, fn } from '@prisma-next/mongo-query-builder';
15
+ import { collection } from '@prisma-next/mongo-query-builder/contract-free';
4
16
  import type { MongoSchemaIR } from '@prisma-next/mongo-schema-ir';
17
+ import type { MongoValue } from '@prisma-next/mongo-value';
18
+ import { blindCast } from '@prisma-next/utils/casts';
19
+ import type { Document } from 'mongodb';
20
+ import { createMongoAdapter } from '../mongo-adapter';
5
21
  import { introspectSchema } from './introspect-schema';
6
22
  import {
7
- initMarker,
8
- readAllMarkers,
9
- readMarker,
10
- updateMarker,
11
- writeLedgerEntry,
23
+ MONGO_LEDGER_COLLECTION,
24
+ MONGO_MARKER_COLLECTION,
25
+ parseMongoMarkerDocSafely,
12
26
  } from './marker-ledger';
27
+ import { MARKER_LEDGER_COLLECTION, type MarkerLedgerDocShape } from './marker-ledger-collection';
28
+ import { isMongoControlDriver } from './mongo-control-driver';
13
29
  import { extractDb } from './runner-deps';
14
30
 
15
31
  /**
16
32
  * Mongo control adapter for control-plane operations like introspection
17
33
  * and marker-ledger CAS. Implements the family-level `MongoControlAdapter`
18
- * SPI by extracting the underlying `Db` from the framework-shaped driver
19
- * and forwarding to the wire-level helpers in this package.
34
+ * SPI. Every marker/ledger operation builds a canonical command inline via
35
+ * the contract-free fluent builder and dispatches it through the family
36
+ * adapter's lowering path (`createMongoAdapter().lower(plan, {})`) onto the
37
+ * Mongo wire transport (`driver.execute(wireCommand)`) — the same route SQL
38
+ * marker/ledger ops take through `adapter.lower()` → `driver.query()`.
20
39
  */
21
40
  export class MongoControlAdapterImpl implements MongoControlAdapter<'mongo'> {
22
41
  readonly familyId = 'mongo' as const;
23
42
  readonly targetId = 'mongo' as const;
43
+ readonly #adapter: MongoAdapter = createMongoAdapter();
44
+ readonly #markerLedgerCollection = collection<MarkerLedgerDocShape>(MARKER_LEDGER_COLLECTION);
45
+
46
+ async #execute(
47
+ driver: ControlDriverInstance<'mongo', 'mongo'>,
48
+ command: AnyMongoCommand,
49
+ ): Promise<Document[]> {
50
+ const plan: MongoQueryPlan = {
51
+ collection: MARKER_LEDGER_COLLECTION,
52
+ command,
53
+ meta: { target: 'mongo', targetFamily: 'mongo', storageHash: '', lane: 'control' },
54
+ };
55
+ const wireCommand = await this.#adapter.lower(plan, {});
56
+ if (!isMongoControlDriver(driver)) {
57
+ throw new Error(
58
+ 'Mongo control adapter requires a Mongo control driver with an execute() transport. ' +
59
+ 'Provide a MongoControlDriver from `@prisma-next/driver-mongo/control`.',
60
+ );
61
+ }
62
+ const rows: Document[] = [];
63
+ for await (const row of driver.execute<Document>(wireCommand)) {
64
+ rows.push(row);
65
+ }
66
+ return rows;
67
+ }
68
+
69
+ /**
70
+ * Server-side invariant-merge aggregation expression:
71
+ * `$sortArray({ input: $setUnion([$ifNull('$invariants', []), incoming]), sortBy: 1 })`.
72
+ *
73
+ * Built through the typed agg-expr layer — `fn.setUnion` plus the generic
74
+ * `MongoAggOperator` for `$ifNull` / `$sortArray` (neither has a named `fn`
75
+ * helper). Returns a `MongoAggOperator` that is passed directly to
76
+ * `f.stage.set({ invariants: ... })` in the update pipeline.
77
+ */
78
+ #invariantMergeExpr(incoming: readonly string[]): MongoAggOperator {
79
+ const existingOrEmpty = MongoAggOperator.of('$ifNull', [
80
+ MongoAggFieldRef.of('invariants'),
81
+ MongoAggLiteral.of([]),
82
+ ]);
83
+ const merged = fn.setUnion(
84
+ { _field: { codecId: 'mongo/array@1', nullable: false }, node: existingOrEmpty },
85
+ {
86
+ _field: { codecId: 'mongo/array@1', nullable: false },
87
+ node: MongoAggLiteral.of([...incoming]),
88
+ },
89
+ );
90
+ return MongoAggOperator.of('$sortArray', { input: merged.node, sortBy: MongoAggLiteral.of(1) });
91
+ }
24
92
 
25
93
  async readMarker(
26
94
  driver: ControlDriverInstance<'mongo', 'mongo'>,
27
95
  space: string,
28
96
  ): Promise<ContractMarkerRecord | null> {
29
- return readMarker(extractDb(driver), space);
97
+ const markerContext = { space, markerLocation: MONGO_MARKER_COLLECTION };
98
+ const docs = await withMarkerReadErrorHandling(
99
+ () =>
100
+ this.#execute(
101
+ driver,
102
+ this.#markerLedgerCollection
103
+ .aggregate()
104
+ .match((f) => f._id.eq(space))
105
+ .match((f) => f.space.eq(space))
106
+ .limit(1)
107
+ .build(),
108
+ ),
109
+ markerContext,
110
+ );
111
+ const doc = docs[0];
112
+ if (!doc) return null;
113
+ return parseMongoMarkerDocSafely(doc, space);
30
114
  }
31
115
 
32
116
  async readAllMarkers(
33
117
  driver: ControlDriverInstance<'mongo', 'mongo'>,
34
118
  ): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
35
- return readAllMarkers(extractDb(driver));
119
+ const markerContext = { space: 'app', markerLocation: MONGO_MARKER_COLLECTION };
120
+ const docs = await withMarkerReadErrorHandling(
121
+ () =>
122
+ this.#execute(
123
+ driver,
124
+ this.#markerLedgerCollection
125
+ .aggregate()
126
+ .match((f) => f._id.type('string'))
127
+ .match((f) => f.space.type('string'))
128
+ .match((f) => expr(fn.eq(f._id, f.space)))
129
+ .build(),
130
+ ),
131
+ markerContext,
132
+ );
133
+ const out = new Map<string, ContractMarkerRecord>();
134
+ for (const doc of docs) {
135
+ const space = doc['space'];
136
+ /* v8 ignore next -- @preserve type-narrowing guard: the $match stage above filters on `space: { $type: 'string' }`, so this branch is unreachable at runtime. The check exists so the `out.set(space, ...)` call below can accept `string`. */
137
+ if (typeof space !== 'string') continue;
138
+ out.set(space, parseMongoMarkerDocSafely(doc, space));
139
+ }
140
+ return out;
36
141
  }
37
142
 
38
143
  async initMarker(
@@ -44,7 +149,19 @@ export class MongoControlAdapterImpl implements MongoControlAdapter<'mongo'> {
44
149
  readonly invariants?: readonly string[];
45
150
  },
46
151
  ): Promise<void> {
47
- await initMarker(extractDb(driver), space, destination);
152
+ const document: Record<string, MongoValue> = {
153
+ _id: space,
154
+ space,
155
+ storageHash: destination.storageHash,
156
+ profileHash: destination.profileHash,
157
+ contractJson: null,
158
+ canonicalVersion: null,
159
+ updatedAt: new Date(),
160
+ appTag: null,
161
+ meta: {},
162
+ invariants: [...(destination.invariants ?? [])],
163
+ };
164
+ await this.#execute(driver, this.#markerLedgerCollection.insertOne(document));
48
165
  }
49
166
 
50
167
  async updateMarker(
@@ -57,15 +174,118 @@ export class MongoControlAdapterImpl implements MongoControlAdapter<'mongo'> {
57
174
  readonly invariants?: readonly string[];
58
175
  },
59
176
  ): Promise<boolean> {
60
- return updateMarker(extractDb(driver), space, expectedFrom, destination);
177
+ const { invariants } = destination;
178
+ const docs = await this.#execute(
179
+ driver,
180
+ this.#markerLedgerCollection
181
+ .match((f) => f._id.eq(space))
182
+ .match((f) => f.space.eq(space))
183
+ .match((f) => f.storageHash.eq(expectedFrom))
184
+ .findOneAndUpdate(
185
+ (f) => [
186
+ f.stage.set(
187
+ invariants === undefined
188
+ ? {
189
+ storageHash: MongoAggLiteral.of(destination.storageHash),
190
+ profileHash: MongoAggLiteral.of(destination.profileHash),
191
+ updatedAt: MongoAggLiteral.of(new Date()),
192
+ }
193
+ : {
194
+ storageHash: MongoAggLiteral.of(destination.storageHash),
195
+ profileHash: MongoAggLiteral.of(destination.profileHash),
196
+ updatedAt: MongoAggLiteral.of(new Date()),
197
+ invariants: this.#invariantMergeExpr(invariants),
198
+ },
199
+ ),
200
+ ],
201
+ { upsert: false },
202
+ ),
203
+ );
204
+ return docs.length > 0;
61
205
  }
62
206
 
63
207
  async writeLedgerEntry(
64
208
  driver: ControlDriverInstance<'mongo', 'mongo'>,
65
209
  space: string,
66
- entry: { readonly edgeId: string; readonly from: string; readonly to: string },
210
+ entry: {
211
+ readonly edgeId: string;
212
+ readonly from: string;
213
+ readonly to: string;
214
+ readonly migrationName: string;
215
+ readonly migrationHash: string;
216
+ readonly operations: readonly unknown[];
217
+ },
67
218
  ): Promise<void> {
68
- await writeLedgerEntry(extractDb(driver), space, entry);
219
+ const document: Record<string, MongoValue> = {
220
+ type: 'ledger',
221
+ space,
222
+ edgeId: entry.edgeId,
223
+ from: entry.from,
224
+ to: entry.to,
225
+ migrationName: entry.migrationName,
226
+ migrationHash: entry.migrationHash,
227
+ operations: blindCast<ReadonlyArray<MongoValue>, 'ledger operation docs are BSON values'>(
228
+ entry.operations,
229
+ ),
230
+ appliedAt: new Date(),
231
+ };
232
+ await this.#execute(driver, this.#markerLedgerCollection.insertOne(document));
233
+ }
234
+
235
+ async readLedger(
236
+ driver: ControlDriverInstance<'mongo', 'mongo'>,
237
+ space?: string,
238
+ ): Promise<readonly LedgerEntryRecord[]> {
239
+ const ledgerContext = { space: space ?? '*', markerLocation: MONGO_LEDGER_COLLECTION };
240
+ const chain = this.#markerLedgerCollection.aggregate().match((f) => f.type.eq('ledger'));
241
+ const command =
242
+ space === undefined
243
+ ? chain.sort({ _id: 1 }).build()
244
+ : chain
245
+ .match((f) => f.space.eq(space))
246
+ .sort({ _id: 1 })
247
+ .build();
248
+ const docs = await withMarkerReadErrorHandling(
249
+ () => this.#execute(driver, command),
250
+ ledgerContext,
251
+ );
252
+
253
+ const entries: LedgerEntryRecord[] = [];
254
+ for (const doc of docs) {
255
+ const migrationName = doc['migrationName'];
256
+ const migrationHash = doc['migrationHash'];
257
+ const from = doc['from'];
258
+ const to = doc['to'];
259
+ const docSpace = doc['space'];
260
+ if (typeof migrationName !== 'string' || typeof migrationHash !== 'string') {
261
+ continue;
262
+ }
263
+ if (typeof from !== 'string' || typeof to !== 'string') {
264
+ continue;
265
+ }
266
+ if (typeof docSpace !== 'string') {
267
+ continue;
268
+ }
269
+ const appliedAt = doc['appliedAt'];
270
+ const appliedAtDate =
271
+ appliedAt instanceof Date
272
+ ? appliedAt
273
+ : appliedAt !== undefined
274
+ ? new Date(String(appliedAt))
275
+ : new Date();
276
+ const operations = doc['operations'];
277
+ const opList = Array.isArray(operations) ? operations : [];
278
+ entries.push({
279
+ space: docSpace,
280
+ migrationName,
281
+ migrationHash,
282
+ from: ledgerOriginFromStored(from),
283
+ to,
284
+ appliedAt: appliedAtDate,
285
+ operationCount: opList.length,
286
+ });
287
+ }
288
+ return entries;
69
289
  }
70
290
 
71
291
  async introspectSchema(driver: ControlDriverInstance<'mongo', 'mongo'>): Promise<MongoSchemaIR> {