@prisma-next/adapter-mongo 0.12.0-dev.5 → 0.12.0-dev.51
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/control.d.mts +18 -66
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +157 -161
- package/dist/control.mjs.map +1 -1
- package/package.json +24 -22
- package/src/core/marker-ledger-collection.ts +32 -0
- package/src/core/marker-ledger.ts +7 -215
- package/src/core/mongo-control-adapter.ts +234 -14
- package/src/core/mongo-control-driver.ts +11 -28
- package/src/core/runner-deps.ts +13 -10
- package/src/exports/control.ts +3 -11
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
19
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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> {
|
|
@@ -1,30 +1,13 @@
|
|
|
1
1
|
import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
constructor(db: Db, client: MongoClient) {
|
|
15
|
-
this.db = db;
|
|
16
|
-
this.#client = client;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
query(): Promise<never> {
|
|
20
|
-
throw new Error('MongoDB control driver does not support SQL queries');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async close(): Promise<void> {
|
|
24
|
-
await this.#client.close();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createMongoControlDriver(db: Db, client: MongoClient): MongoControlDriverInstance {
|
|
29
|
-
return new MongoControlDriverImpl(db, client);
|
|
2
|
+
import type { MongoControlDriverInstance } from '@prisma-next/mongo-lowering';
|
|
3
|
+
|
|
4
|
+
export function isMongoControlDriver(
|
|
5
|
+
driver: ControlDriverInstance<'mongo', string>,
|
|
6
|
+
): driver is MongoControlDriverInstance {
|
|
7
|
+
return (
|
|
8
|
+
driver.familyId === 'mongo' &&
|
|
9
|
+
driver.targetId === 'mongo' &&
|
|
10
|
+
'execute' in driver &&
|
|
11
|
+
typeof driver.execute === 'function'
|
|
12
|
+
);
|
|
30
13
|
}
|
package/src/core/runner-deps.ts
CHANGED
|
@@ -14,16 +14,16 @@ import type { Db } from 'mongodb';
|
|
|
14
14
|
import { createMongoAdapter } from '../mongo-adapter';
|
|
15
15
|
import { MongoCommandExecutor, MongoInspectionExecutor } from './command-executor';
|
|
16
16
|
import { MongoControlAdapterImpl } from './mongo-control-adapter';
|
|
17
|
+
import { isMongoControlDriver } from './mongo-control-driver';
|
|
17
18
|
|
|
18
19
|
export function extractDb(driver: ControlDriverInstance<'mongo', 'mongo'>): Db {
|
|
19
|
-
|
|
20
|
-
if (!mongoDriver.db) {
|
|
20
|
+
if (!isMongoControlDriver(driver)) {
|
|
21
21
|
throw new Error(
|
|
22
|
-
'Mongo control driver
|
|
23
|
-
'
|
|
22
|
+
'Expected a Mongo control driver created by ' +
|
|
23
|
+
'mongoControlDriver.create() from `@prisma-next/driver-mongo/control`.',
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
|
-
return
|
|
26
|
+
return driver.db;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -57,6 +57,9 @@ export interface MarkerOperations {
|
|
|
57
57
|
readonly edgeId: string;
|
|
58
58
|
readonly from: string;
|
|
59
59
|
readonly to: string;
|
|
60
|
+
readonly migrationName: string;
|
|
61
|
+
readonly migrationHash: string;
|
|
62
|
+
readonly operations: readonly unknown[];
|
|
60
63
|
},
|
|
61
64
|
): Promise<void>;
|
|
62
65
|
}
|
|
@@ -82,11 +85,11 @@ export interface MongoRunnerDependencies {
|
|
|
82
85
|
export function createMongoRunnerDeps(
|
|
83
86
|
controlDriver: ControlDriverInstance<'mongo', 'mongo'>,
|
|
84
87
|
driver: MongoDriver,
|
|
85
|
-
// Vestigial after the
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
88
|
+
// Vestigial after the family→adapter SPI refactor: the runner dependencies
|
|
89
|
+
// now route every wire-level call through `controlAdapter`, so the `family`
|
|
90
|
+
// instance is no longer consulted. Kept on the signature to avoid rippling
|
|
91
|
+
// through ~14 call sites; a follow-up that already touches this factory
|
|
92
|
+
// should drop the parameter outright.
|
|
90
93
|
_family: ControlFamilyInstance<'mongo', MongoSchemaIR>,
|
|
91
94
|
controlAdapter: MongoControlAdapter<'mongo'> = new MongoControlAdapterImpl(),
|
|
92
95
|
): MongoRunnerDependencies {
|
package/src/exports/control.ts
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import type { MongoControlAdapterDescriptor } from '@prisma-next/family-mongo/control-adapter';
|
|
2
|
+
import type { MongoControlDriverInstance } from '@prisma-next/mongo-lowering';
|
|
2
3
|
|
|
3
4
|
export { MongoCommandExecutor, MongoInspectionExecutor } from '../core/command-executor';
|
|
4
5
|
export { introspectSchema } from '../core/introspect-schema';
|
|
5
|
-
export {
|
|
6
|
-
initMarker,
|
|
7
|
-
readAllMarkers,
|
|
8
|
-
readMarker,
|
|
9
|
-
updateMarker,
|
|
10
|
-
writeLedgerEntry,
|
|
11
|
-
} from '../core/marker-ledger';
|
|
12
6
|
export { MongoControlAdapterImpl } from '../core/mongo-control-adapter';
|
|
13
|
-
export {
|
|
14
|
-
createMongoControlDriver,
|
|
15
|
-
type MongoControlDriverInstance,
|
|
16
|
-
} from '../core/mongo-control-driver';
|
|
7
|
+
export { isMongoControlDriver } from '../core/mongo-control-driver';
|
|
17
8
|
export {
|
|
18
9
|
createMongoRunnerDeps,
|
|
19
10
|
extractDb,
|
|
@@ -21,6 +12,7 @@ export {
|
|
|
21
12
|
type MongoRunnerDependencies,
|
|
22
13
|
} from '../core/runner-deps';
|
|
23
14
|
export { createMongoAdapter } from '../mongo-adapter';
|
|
15
|
+
export type { MongoControlDriverInstance };
|
|
24
16
|
|
|
25
17
|
import { mongoCodecDescriptors } from '../core/codecs';
|
|
26
18
|
import { MongoControlAdapterImpl } from '../core/mongo-control-adapter';
|