@prisma-next/family-mongo 0.7.0 → 0.8.0-dev.1
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/README.md +22 -9
- package/dist/control-adapter.d.mts +75 -0
- package/dist/control-adapter.d.mts.map +1 -0
- package/dist/control-adapter.mjs +1 -0
- package/dist/control.d.mts +60 -16
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +257 -276
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +131 -0
- package/dist/ir.d.mts.map +1 -0
- package/dist/ir.mjs +54 -0
- package/dist/ir.mjs.map +1 -0
- package/dist/mongo-contract-serializer-Co3EaTVj.mjs +98 -0
- package/dist/mongo-contract-serializer-Co3EaTVj.mjs.map +1 -0
- package/dist/schema-verify.d.mts +22 -2
- package/dist/schema-verify.d.mts.map +1 -0
- package/dist/schema-verify.mjs +1 -1
- package/dist/verify-mongo-schema-BL7t9YTB.mjs +592 -0
- package/dist/verify-mongo-schema-BL7t9YTB.mjs.map +1 -0
- package/package.json +20 -22
- package/src/core/contract-to-schema.ts +84 -0
- package/src/core/control-adapter.ts +97 -0
- package/src/core/control-instance.ts +275 -272
- package/src/core/control-target-descriptor.ts +26 -0
- package/src/core/control-types.ts +6 -4
- package/src/core/ir/mongo-contract-serializer-base.ts +124 -0
- package/src/core/ir/mongo-contract-serializer.ts +18 -0
- package/src/core/ir/mongo-schema-verifier-base.ts +87 -0
- package/src/core/operation-preview.ts +131 -0
- package/src/core/schema-diff.ts +402 -0
- package/src/core/schema-verify/canonicalize-introspection.ts +389 -0
- package/src/core/schema-verify/verify-mongo-schema.ts +60 -0
- package/src/exports/control-adapter.ts +1 -0
- package/src/exports/control.ts +8 -1
- package/src/exports/ir.ts +3 -0
- package/src/exports/schema-verify.ts +4 -2
- package/src/core/mongo-target-descriptor.ts +0 -180
package/dist/control.mjs
CHANGED
|
@@ -1,16 +1,90 @@
|
|
|
1
|
+
import { i as contractToMongoSchemaIR, n as canonicalizeSchemasForVerification, r as diffMongoSchemas, t as verifyMongoSchema } from "./verify-mongo-schema-BL7t9YTB.mjs";
|
|
2
|
+
import { t as MongoContractSerializer } from "./mongo-contract-serializer-Co3EaTVj.mjs";
|
|
1
3
|
import { mongoEmission } from "@prisma-next/mongo-emitter";
|
|
2
|
-
import { createMongoRunnerDeps, extractDb, introspectSchema } from "@prisma-next/adapter-mongo/control";
|
|
3
4
|
import { APP_SPACE_ID, SchemaTreeNode, VERIFY_CODE_HASH_MISMATCH, VERIFY_CODE_MARKER_MISSING, VERIFY_CODE_TARGET_MISMATCH } from "@prisma-next/framework-components/control";
|
|
4
5
|
import { assertDescriptorSelfConsistency } from "@prisma-next/migration-tools/spaces";
|
|
5
|
-
import { validateMongoContract } from "@prisma-next/mongo-contract";
|
|
6
|
-
import { MongoMigrationPlanner, MongoMigrationRunner, contractToMongoSchemaIR, formatMongoOperations, initMarker, readAllMarkers, readMarker, updateMarker } from "@prisma-next/target-mongo/control";
|
|
7
|
-
import { verifyMongoSchema } from "@prisma-next/target-mongo/schema-verify";
|
|
8
6
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
//#region src/core/operation-preview.ts
|
|
8
|
+
function formatKeySpec(keys) {
|
|
9
|
+
return `{ ${keys.map((k) => `${JSON.stringify(k.field)}: ${JSON.stringify(k.direction)}`).join(", ")} }`;
|
|
10
|
+
}
|
|
11
|
+
function formatOptions(cmd) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
if (cmd.unique) parts.push("unique: true");
|
|
14
|
+
if (cmd.sparse) parts.push("sparse: true");
|
|
15
|
+
if (cmd.expireAfterSeconds !== void 0) parts.push(`expireAfterSeconds: ${cmd.expireAfterSeconds}`);
|
|
16
|
+
if (cmd.name) parts.push(`name: ${JSON.stringify(cmd.name)}`);
|
|
17
|
+
if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
|
|
18
|
+
if (cmd.weights) parts.push(`weights: ${JSON.stringify(cmd.weights)}`);
|
|
19
|
+
if (cmd.default_language) parts.push(`default_language: ${JSON.stringify(cmd.default_language)}`);
|
|
20
|
+
if (cmd.language_override) parts.push(`language_override: ${JSON.stringify(cmd.language_override)}`);
|
|
21
|
+
if (cmd.wildcardProjection) parts.push(`wildcardProjection: ${JSON.stringify(cmd.wildcardProjection)}`);
|
|
22
|
+
if (cmd.partialFilterExpression) parts.push(`partialFilterExpression: ${JSON.stringify(cmd.partialFilterExpression)}`);
|
|
23
|
+
if (parts.length === 0) return void 0;
|
|
24
|
+
return `{ ${parts.join(", ")} }`;
|
|
25
|
+
}
|
|
26
|
+
function formatCreateCollectionOptions(cmd) {
|
|
27
|
+
const parts = [];
|
|
28
|
+
if (cmd.capped) parts.push("capped: true");
|
|
29
|
+
if (cmd.size !== void 0) parts.push(`size: ${cmd.size}`);
|
|
30
|
+
if (cmd.max !== void 0) parts.push(`max: ${cmd.max}`);
|
|
31
|
+
if (cmd.timeseries) parts.push(`timeseries: ${JSON.stringify(cmd.timeseries)}`);
|
|
32
|
+
if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
|
|
33
|
+
if (cmd.clusteredIndex) parts.push(`clusteredIndex: ${JSON.stringify(cmd.clusteredIndex)}`);
|
|
34
|
+
if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
|
|
35
|
+
if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
|
|
36
|
+
if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
|
|
37
|
+
if (cmd.changeStreamPreAndPostImages) parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
|
|
38
|
+
if (parts.length === 0) return void 0;
|
|
39
|
+
return `{ ${parts.join(", ")} }`;
|
|
40
|
+
}
|
|
41
|
+
var MongoDdlCommandFormatter = class {
|
|
42
|
+
createIndex(cmd) {
|
|
43
|
+
const keySpec = formatKeySpec(cmd.keys);
|
|
44
|
+
const opts = formatOptions(cmd);
|
|
45
|
+
return opts ? `db.${cmd.collection}.createIndex(${keySpec}, ${opts})` : `db.${cmd.collection}.createIndex(${keySpec})`;
|
|
46
|
+
}
|
|
47
|
+
dropIndex(cmd) {
|
|
48
|
+
return `db.${cmd.collection}.dropIndex(${JSON.stringify(cmd.name)})`;
|
|
49
|
+
}
|
|
50
|
+
createCollection(cmd) {
|
|
51
|
+
const opts = formatCreateCollectionOptions(cmd);
|
|
52
|
+
return opts ? `db.createCollection(${JSON.stringify(cmd.collection)}, ${opts})` : `db.createCollection(${JSON.stringify(cmd.collection)})`;
|
|
53
|
+
}
|
|
54
|
+
dropCollection(cmd) {
|
|
55
|
+
return `db.${cmd.collection}.drop()`;
|
|
56
|
+
}
|
|
57
|
+
collMod(cmd) {
|
|
58
|
+
const parts = [`collMod: ${JSON.stringify(cmd.collection)}`];
|
|
59
|
+
if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
|
|
60
|
+
if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
|
|
61
|
+
if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
|
|
62
|
+
if (cmd.changeStreamPreAndPostImages) parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
|
|
63
|
+
return `db.runCommand({ ${parts.join(", ")} })`;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const formatter = new MongoDdlCommandFormatter();
|
|
67
|
+
function formatMongoOperations(operations) {
|
|
68
|
+
const statements = [];
|
|
69
|
+
for (const operation of operations) {
|
|
70
|
+
const candidate = operation;
|
|
71
|
+
if (!("execute" in candidate) || !Array.isArray(candidate["execute"])) continue;
|
|
72
|
+
for (const step of candidate["execute"]) if (step.command && typeof step.command.accept === "function") statements.push(step.command.accept(formatter));
|
|
73
|
+
}
|
|
74
|
+
return statements;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Wraps `formatMongoOperations` into the family-agnostic
|
|
78
|
+
* `OperationPreview` shape. Each statement carries
|
|
79
|
+
* `language: 'mongodb-shell'`. Mirrors `sqlOperationsToPreview`.
|
|
80
|
+
*/
|
|
81
|
+
function mongoOperationsToPreview(operations) {
|
|
82
|
+
return { statements: formatMongoOperations(operations).map((text) => ({
|
|
83
|
+
text,
|
|
84
|
+
language: "mongodb-shell"
|
|
85
|
+
})) };
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
14
88
|
//#region src/core/schema-to-view.ts
|
|
15
89
|
function mongoSchemaToView(schema) {
|
|
16
90
|
const collectionNodes = schema.collections.map((collection) => collectionToSchemaNode(collection.name, collection));
|
|
@@ -104,170 +178,29 @@ function collectionToSchemaNode(name, collection) {
|
|
|
104
178
|
}
|
|
105
179
|
//#endregion
|
|
106
180
|
//#region src/core/control-instance.ts
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
181
|
+
function deserializeMongoContract(contractJson) {
|
|
182
|
+
return new MongoContractSerializer().deserializeContract(contractJson);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Family-method contract input. By the time control-plane methods
|
|
186
|
+
* (`verify`, `verifySchema`, `sign`, …) are invoked through the CLI
|
|
187
|
+
* control client (`client.ts`), the input has already been threaded
|
|
188
|
+
* through `familyInstance.validateContract`. The value is therefore a
|
|
189
|
+
* class-form `MongoTargetContract` (or a structurally-equivalent
|
|
190
|
+
* envelope post-deserialization) and must NOT be re-fed through
|
|
191
|
+
* structural validation (arktype rejects extra keys like `namespaces`).
|
|
192
|
+
*
|
|
193
|
+
* The parameter type on the framework SPI is `unknown` for variance
|
|
194
|
+
* reasons (so the family can express its own contract type without
|
|
195
|
+
* leaking it to the framework). This helper recovers the validated
|
|
196
|
+
* shape with a single narrow cast.
|
|
197
|
+
*/
|
|
198
|
+
function asValidatedMongoContract(contract) {
|
|
199
|
+
return contract;
|
|
200
|
+
}
|
|
201
|
+
function isMongoControlAdapter(value) {
|
|
202
|
+
return typeof value === "object" && value !== null && "readMarker" in value && typeof value.readMarker === "function" && "readAllMarkers" in value && typeof value.readAllMarkers === "function" && "introspectSchema" in value && typeof value.introspectSchema === "function";
|
|
111
203
|
}
|
|
112
|
-
var MongoFamilyInstance = class {
|
|
113
|
-
familyId = "mongo";
|
|
114
|
-
validateContract(contractJson) {
|
|
115
|
-
return validateMongoContract(contractJson).contract;
|
|
116
|
-
}
|
|
117
|
-
async verify(options) {
|
|
118
|
-
const { driver, contract: rawContract, expectedTargetId, contractPath, configPath } = options;
|
|
119
|
-
const startTime = Date.now();
|
|
120
|
-
const contract = validateMongoContract(rawContract).contract;
|
|
121
|
-
const contractStorageHash = contract.storage.storageHash;
|
|
122
|
-
const contractProfileHash = contract.profileHash;
|
|
123
|
-
const contractTarget = contract.target;
|
|
124
|
-
const baseOpts = {
|
|
125
|
-
contractStorageHash,
|
|
126
|
-
contractProfileHash,
|
|
127
|
-
expectedTargetId,
|
|
128
|
-
contractPath,
|
|
129
|
-
...ifDefined("configPath", configPath)
|
|
130
|
-
};
|
|
131
|
-
if (contractTarget !== expectedTargetId) return buildVerifyResult({
|
|
132
|
-
...baseOpts,
|
|
133
|
-
ok: false,
|
|
134
|
-
code: VERIFY_CODE_TARGET_MISMATCH,
|
|
135
|
-
summary: "Target mismatch",
|
|
136
|
-
actualTargetId: contractTarget,
|
|
137
|
-
totalTime: Date.now() - startTime
|
|
138
|
-
});
|
|
139
|
-
const marker = await readMarker(extractDb$1(driver), APP_SPACE_ID);
|
|
140
|
-
if (!marker) return buildVerifyResult({
|
|
141
|
-
...baseOpts,
|
|
142
|
-
ok: false,
|
|
143
|
-
code: VERIFY_CODE_MARKER_MISSING,
|
|
144
|
-
summary: "Marker missing",
|
|
145
|
-
totalTime: Date.now() - startTime
|
|
146
|
-
});
|
|
147
|
-
if (marker.storageHash !== contractStorageHash) return buildVerifyResult({
|
|
148
|
-
...baseOpts,
|
|
149
|
-
ok: false,
|
|
150
|
-
code: VERIFY_CODE_HASH_MISMATCH,
|
|
151
|
-
summary: "Hash mismatch",
|
|
152
|
-
marker,
|
|
153
|
-
totalTime: Date.now() - startTime
|
|
154
|
-
});
|
|
155
|
-
if (contractProfileHash && marker.profileHash !== contractProfileHash) return buildVerifyResult({
|
|
156
|
-
...baseOpts,
|
|
157
|
-
ok: false,
|
|
158
|
-
code: VERIFY_CODE_HASH_MISMATCH,
|
|
159
|
-
summary: "Hash mismatch",
|
|
160
|
-
marker,
|
|
161
|
-
totalTime: Date.now() - startTime
|
|
162
|
-
});
|
|
163
|
-
return buildVerifyResult({
|
|
164
|
-
...baseOpts,
|
|
165
|
-
ok: true,
|
|
166
|
-
summary: "Database matches contract",
|
|
167
|
-
marker,
|
|
168
|
-
totalTime: Date.now() - startTime
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
async schemaVerify(options) {
|
|
172
|
-
const { driver, contract: rawContract, strict, contractPath, configPath } = options;
|
|
173
|
-
const contract = validateMongoContract(rawContract).contract;
|
|
174
|
-
return verifyMongoSchema({
|
|
175
|
-
contract,
|
|
176
|
-
schema: await introspectSchema(extractDb$1(driver)),
|
|
177
|
-
strict,
|
|
178
|
-
frameworkComponents: options.frameworkComponents,
|
|
179
|
-
context: {
|
|
180
|
-
contractPath,
|
|
181
|
-
...ifDefined("configPath", configPath)
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
schemaVerifyAgainstSchema(options) {
|
|
186
|
-
return verifyMongoSchema({
|
|
187
|
-
contract: validateMongoContract(options.contract).contract,
|
|
188
|
-
schema: options.schema,
|
|
189
|
-
strict: options.strict,
|
|
190
|
-
frameworkComponents: options.frameworkComponents
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
async sign(options) {
|
|
194
|
-
const { driver, contract: rawContract, contractPath, configPath } = options;
|
|
195
|
-
const startTime = Date.now();
|
|
196
|
-
const contract = validateMongoContract(rawContract).contract;
|
|
197
|
-
const contractStorageHash = contract.storage.storageHash;
|
|
198
|
-
const contractProfileHash = contract.profileHash;
|
|
199
|
-
const db = extractDb$1(driver);
|
|
200
|
-
const existingMarker = await readMarker(db, APP_SPACE_ID);
|
|
201
|
-
let markerCreated = false;
|
|
202
|
-
let markerUpdated = false;
|
|
203
|
-
let previousHashes;
|
|
204
|
-
if (!existingMarker) {
|
|
205
|
-
await initMarker(db, APP_SPACE_ID, {
|
|
206
|
-
storageHash: contractStorageHash,
|
|
207
|
-
profileHash: contractProfileHash
|
|
208
|
-
});
|
|
209
|
-
markerCreated = true;
|
|
210
|
-
} else {
|
|
211
|
-
const storageHashMatches = existingMarker.storageHash === contractStorageHash;
|
|
212
|
-
const profileHashMatches = existingMarker.profileHash === contractProfileHash;
|
|
213
|
-
if (!storageHashMatches || !profileHashMatches) {
|
|
214
|
-
previousHashes = {
|
|
215
|
-
storageHash: existingMarker.storageHash,
|
|
216
|
-
profileHash: existingMarker.profileHash
|
|
217
|
-
};
|
|
218
|
-
if (!await updateMarker(db, APP_SPACE_ID, existingMarker.storageHash, {
|
|
219
|
-
storageHash: contractStorageHash,
|
|
220
|
-
profileHash: contractProfileHash
|
|
221
|
-
})) throw new Error("CAS conflict: marker was modified by another process during sign");
|
|
222
|
-
markerUpdated = true;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
let summary;
|
|
226
|
-
if (markerCreated) summary = "Database signed (marker created)";
|
|
227
|
-
else if (markerUpdated) summary = `Database signed (marker updated from ${previousHashes?.storageHash ?? "unknown"})`;
|
|
228
|
-
else summary = "Database already signed with this contract";
|
|
229
|
-
return {
|
|
230
|
-
ok: true,
|
|
231
|
-
summary,
|
|
232
|
-
contract: {
|
|
233
|
-
storageHash: contractStorageHash,
|
|
234
|
-
profileHash: contractProfileHash
|
|
235
|
-
},
|
|
236
|
-
target: {
|
|
237
|
-
expected: contract.target,
|
|
238
|
-
actual: contract.target
|
|
239
|
-
},
|
|
240
|
-
marker: {
|
|
241
|
-
created: markerCreated,
|
|
242
|
-
updated: markerUpdated,
|
|
243
|
-
...ifDefined("previous", previousHashes)
|
|
244
|
-
},
|
|
245
|
-
meta: {
|
|
246
|
-
contractPath,
|
|
247
|
-
...ifDefined("configPath", configPath)
|
|
248
|
-
},
|
|
249
|
-
timings: { total: Date.now() - startTime }
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
async readMarker(options) {
|
|
253
|
-
return readMarker(extractDb$1(options.driver), options.space);
|
|
254
|
-
}
|
|
255
|
-
async readAllMarkers(options) {
|
|
256
|
-
return readAllMarkers(extractDb$1(options.driver));
|
|
257
|
-
}
|
|
258
|
-
async introspect(options) {
|
|
259
|
-
return introspectSchema(extractDb$1(options.driver));
|
|
260
|
-
}
|
|
261
|
-
toSchemaView(schema) {
|
|
262
|
-
return mongoSchemaToView(schema);
|
|
263
|
-
}
|
|
264
|
-
toOperationPreview(operations) {
|
|
265
|
-
return { statements: formatMongoOperations(operations).map((text) => ({
|
|
266
|
-
text,
|
|
267
|
-
language: "mongodb-shell"
|
|
268
|
-
})) };
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
204
|
function buildVerifyResult(opts) {
|
|
272
205
|
return {
|
|
273
206
|
ok: opts.ok,
|
|
@@ -304,7 +237,157 @@ function createMongoFamilyInstance(controlStack) {
|
|
|
304
237
|
headRefHash: headRef.hash
|
|
305
238
|
});
|
|
306
239
|
}
|
|
307
|
-
|
|
240
|
+
const adapter = controlStack.adapter;
|
|
241
|
+
const getControlAdapter = () => {
|
|
242
|
+
if (!adapter) throw new Error("Mongo family requires an adapter descriptor in ControlStack");
|
|
243
|
+
const controlAdapter = adapter.create(controlStack);
|
|
244
|
+
if (!isMongoControlAdapter(controlAdapter)) throw new Error("Adapter does not implement MongoControlAdapter (missing readMarker, readAllMarkers, or introspectSchema)");
|
|
245
|
+
return controlAdapter;
|
|
246
|
+
};
|
|
247
|
+
const asMongoDriver = (driver) => driver;
|
|
248
|
+
return {
|
|
249
|
+
familyId: "mongo",
|
|
250
|
+
validateContract(contractJson) {
|
|
251
|
+
return deserializeMongoContract(contractJson);
|
|
252
|
+
},
|
|
253
|
+
async verify(options) {
|
|
254
|
+
const { driver, contract: rawContract, expectedTargetId, contractPath, configPath } = options;
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
const contract = asValidatedMongoContract(rawContract);
|
|
257
|
+
const contractStorageHash = contract.storage.storageHash;
|
|
258
|
+
const contractProfileHash = contract.profileHash;
|
|
259
|
+
const contractTarget = contract.target;
|
|
260
|
+
const baseOpts = {
|
|
261
|
+
contractStorageHash,
|
|
262
|
+
contractProfileHash,
|
|
263
|
+
expectedTargetId,
|
|
264
|
+
contractPath,
|
|
265
|
+
...ifDefined("configPath", configPath)
|
|
266
|
+
};
|
|
267
|
+
if (contractTarget !== expectedTargetId) return buildVerifyResult({
|
|
268
|
+
...baseOpts,
|
|
269
|
+
ok: false,
|
|
270
|
+
code: VERIFY_CODE_TARGET_MISMATCH,
|
|
271
|
+
summary: "Target mismatch",
|
|
272
|
+
actualTargetId: contractTarget,
|
|
273
|
+
totalTime: Date.now() - startTime
|
|
274
|
+
});
|
|
275
|
+
const marker = await getControlAdapter().readMarker(asMongoDriver(driver), APP_SPACE_ID);
|
|
276
|
+
if (!marker) return buildVerifyResult({
|
|
277
|
+
...baseOpts,
|
|
278
|
+
ok: false,
|
|
279
|
+
code: VERIFY_CODE_MARKER_MISSING,
|
|
280
|
+
summary: "Marker missing",
|
|
281
|
+
totalTime: Date.now() - startTime
|
|
282
|
+
});
|
|
283
|
+
if (marker.storageHash !== contractStorageHash) return buildVerifyResult({
|
|
284
|
+
...baseOpts,
|
|
285
|
+
ok: false,
|
|
286
|
+
code: VERIFY_CODE_HASH_MISMATCH,
|
|
287
|
+
summary: "Hash mismatch",
|
|
288
|
+
marker,
|
|
289
|
+
totalTime: Date.now() - startTime
|
|
290
|
+
});
|
|
291
|
+
if (contractProfileHash && marker.profileHash !== contractProfileHash) return buildVerifyResult({
|
|
292
|
+
...baseOpts,
|
|
293
|
+
ok: false,
|
|
294
|
+
code: VERIFY_CODE_HASH_MISMATCH,
|
|
295
|
+
summary: "Hash mismatch",
|
|
296
|
+
marker,
|
|
297
|
+
totalTime: Date.now() - startTime
|
|
298
|
+
});
|
|
299
|
+
return buildVerifyResult({
|
|
300
|
+
...baseOpts,
|
|
301
|
+
ok: true,
|
|
302
|
+
summary: "Database matches contract",
|
|
303
|
+
marker,
|
|
304
|
+
totalTime: Date.now() - startTime
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
verifySchema(options) {
|
|
308
|
+
return verifyMongoSchema({
|
|
309
|
+
contract: asValidatedMongoContract(options.contract),
|
|
310
|
+
schema: options.schema,
|
|
311
|
+
strict: options.strict,
|
|
312
|
+
frameworkComponents: options.frameworkComponents
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
async sign(options) {
|
|
316
|
+
const { driver, contract: rawContract, contractPath, configPath } = options;
|
|
317
|
+
const startTime = Date.now();
|
|
318
|
+
const contract = asValidatedMongoContract(rawContract);
|
|
319
|
+
const contractStorageHash = contract.storage.storageHash;
|
|
320
|
+
const contractProfileHash = contract.profileHash;
|
|
321
|
+
const controlAdapter = getControlAdapter();
|
|
322
|
+
const mongoDriver = asMongoDriver(driver);
|
|
323
|
+
const existingMarker = await controlAdapter.readMarker(mongoDriver, APP_SPACE_ID);
|
|
324
|
+
let markerCreated = false;
|
|
325
|
+
let markerUpdated = false;
|
|
326
|
+
let previousHashes;
|
|
327
|
+
if (!existingMarker) {
|
|
328
|
+
await controlAdapter.initMarker(mongoDriver, APP_SPACE_ID, {
|
|
329
|
+
storageHash: contractStorageHash,
|
|
330
|
+
profileHash: contractProfileHash
|
|
331
|
+
});
|
|
332
|
+
markerCreated = true;
|
|
333
|
+
} else {
|
|
334
|
+
const storageHashMatches = existingMarker.storageHash === contractStorageHash;
|
|
335
|
+
const profileHashMatches = existingMarker.profileHash === contractProfileHash;
|
|
336
|
+
if (!storageHashMatches || !profileHashMatches) {
|
|
337
|
+
previousHashes = {
|
|
338
|
+
storageHash: existingMarker.storageHash,
|
|
339
|
+
profileHash: existingMarker.profileHash
|
|
340
|
+
};
|
|
341
|
+
if (!await controlAdapter.updateMarker(mongoDriver, APP_SPACE_ID, existingMarker.storageHash, {
|
|
342
|
+
storageHash: contractStorageHash,
|
|
343
|
+
profileHash: contractProfileHash
|
|
344
|
+
})) throw new Error("CAS conflict: marker was modified by another process during sign");
|
|
345
|
+
markerUpdated = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
let summary;
|
|
349
|
+
if (markerCreated) summary = "Database signed (marker created)";
|
|
350
|
+
else if (markerUpdated) summary = `Database signed (marker updated from ${previousHashes?.storageHash ?? "unknown"})`;
|
|
351
|
+
else summary = "Database already signed with this contract";
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
summary,
|
|
355
|
+
contract: {
|
|
356
|
+
storageHash: contractStorageHash,
|
|
357
|
+
profileHash: contractProfileHash
|
|
358
|
+
},
|
|
359
|
+
target: {
|
|
360
|
+
expected: contract.target,
|
|
361
|
+
actual: contract.target
|
|
362
|
+
},
|
|
363
|
+
marker: {
|
|
364
|
+
created: markerCreated,
|
|
365
|
+
updated: markerUpdated,
|
|
366
|
+
...ifDefined("previous", previousHashes)
|
|
367
|
+
},
|
|
368
|
+
meta: {
|
|
369
|
+
contractPath,
|
|
370
|
+
...ifDefined("configPath", configPath)
|
|
371
|
+
},
|
|
372
|
+
timings: { total: Date.now() - startTime }
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
async readMarker(options) {
|
|
376
|
+
return getControlAdapter().readMarker(asMongoDriver(options.driver), options.space);
|
|
377
|
+
},
|
|
378
|
+
async readAllMarkers(options) {
|
|
379
|
+
return getControlAdapter().readAllMarkers(asMongoDriver(options.driver));
|
|
380
|
+
},
|
|
381
|
+
async introspect(options) {
|
|
382
|
+
return getControlAdapter().introspectSchema(asMongoDriver(options.driver));
|
|
383
|
+
},
|
|
384
|
+
toSchemaView(schema) {
|
|
385
|
+
return mongoSchemaToView(schema);
|
|
386
|
+
},
|
|
387
|
+
toOperationPreview(operations) {
|
|
388
|
+
return mongoOperationsToPreview(operations);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
308
391
|
}
|
|
309
392
|
//#endregion
|
|
310
393
|
//#region src/core/control-descriptor.ts
|
|
@@ -320,108 +403,6 @@ var MongoFamilyDescriptor = class {
|
|
|
320
403
|
};
|
|
321
404
|
const mongoFamilyDescriptor = new MongoFamilyDescriptor();
|
|
322
405
|
//#endregion
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* `migration.ts` default-exports a `Migration` subclass whose `operations`
|
|
326
|
-
* getter returns the ordered list of operations and whose `describe()`
|
|
327
|
-
* returns the manifest identity metadata. `MongoMigrationPlanner.plan()`
|
|
328
|
-
* returns a `MigrationPlanWithAuthoringSurface` that knows how to render
|
|
329
|
-
* itself back to such a file; `MongoMigrationPlanner.emptyMigration()`
|
|
330
|
-
* returns the same shape for `migration new`. Users run the scaffolded
|
|
331
|
-
* `migration.ts` directly (via `node migration.ts`) to self-emit
|
|
332
|
-
* `ops.json` and attest the `migrationHash`.
|
|
333
|
-
*/
|
|
334
|
-
const mongoTargetDescriptor = {
|
|
335
|
-
...mongoTargetDescriptorMeta,
|
|
336
|
-
migrations: {
|
|
337
|
-
createPlanner(_family) {
|
|
338
|
-
return new MongoMigrationPlanner();
|
|
339
|
-
},
|
|
340
|
-
createRunner(family) {
|
|
341
|
-
let cachedDeps;
|
|
342
|
-
const runMongo = async (driver, runnerOptions) => {
|
|
343
|
-
cachedDeps ??= createMongoRunnerDeps(driver, MongoDriverImpl.fromDb(extractDb(driver)), family);
|
|
344
|
-
return new MongoMigrationRunner(cachedDeps).execute({
|
|
345
|
-
...runnerOptions,
|
|
346
|
-
destinationContract: runnerOptions.destinationContract
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
return {
|
|
350
|
-
async execute(options) {
|
|
351
|
-
const { driver, ...runnerOptions } = options;
|
|
352
|
-
return runMongo(driver, runnerOptions);
|
|
353
|
-
},
|
|
354
|
-
async executeAcrossSpaces({ driver, perSpaceOptions }) {
|
|
355
|
-
const members = perSpaceOptions.map(toSpaceMember);
|
|
356
|
-
const perSpaceResults = [];
|
|
357
|
-
for (let i = 0; i < perSpaceOptions.length; i++) {
|
|
358
|
-
const spaceOptions = perSpaceOptions[i];
|
|
359
|
-
if (!spaceOptions) continue;
|
|
360
|
-
const member = members[i];
|
|
361
|
-
if (!member) continue;
|
|
362
|
-
const others = members.filter((_, j) => j !== i);
|
|
363
|
-
const projectSchema = (schema) => {
|
|
364
|
-
return new MongoSchemaIR(projectSchemaToSpace(schema, member, others).collections);
|
|
365
|
-
};
|
|
366
|
-
const result = await runMongo(driver, {
|
|
367
|
-
...spaceOptions,
|
|
368
|
-
projectSchema
|
|
369
|
-
});
|
|
370
|
-
if (!result.ok) return notOk({
|
|
371
|
-
...result.failure,
|
|
372
|
-
failingSpace: spaceOptions.space
|
|
373
|
-
});
|
|
374
|
-
perSpaceResults.push({
|
|
375
|
-
space: spaceOptions.space,
|
|
376
|
-
value: result.value
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
return ok({ perSpaceResults });
|
|
380
|
-
}
|
|
381
|
-
};
|
|
382
|
-
},
|
|
383
|
-
contractToSchema(contract) {
|
|
384
|
-
return contractToMongoSchemaIR(contract);
|
|
385
|
-
}
|
|
386
|
-
},
|
|
387
|
-
create() {
|
|
388
|
-
return {
|
|
389
|
-
familyId: "mongo",
|
|
390
|
-
targetId: "mongo"
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
/**
|
|
395
|
-
* Synthesise the minimum {@link projectSchemaToSpace}-compatible
|
|
396
|
-
* `ContractSpaceMember` shape from a per-space option entry. The
|
|
397
|
-
* projector only reads `spaceId` and `contract.storage`; the rest of
|
|
398
|
-
* `ContractSpaceMember` (head ref invariants, hydrated migration
|
|
399
|
-
* graph) is irrelevant at runner time and stubbed with sentinels.
|
|
400
|
-
*
|
|
401
|
-
* The `as unknown as ContractSpaceMember` cast is the load-bearing bit
|
|
402
|
-
* — the projector duck-types its members so a sentinel-shaped graph
|
|
403
|
-
* never gets read, but the framework type carries a richer shape.
|
|
404
|
-
*/
|
|
405
|
-
function toSpaceMember(opts) {
|
|
406
|
-
return {
|
|
407
|
-
spaceId: opts.space,
|
|
408
|
-
contract: opts.destinationContract,
|
|
409
|
-
headRef: {
|
|
410
|
-
hash: "",
|
|
411
|
-
invariants: []
|
|
412
|
-
},
|
|
413
|
-
migrations: {
|
|
414
|
-
graph: {
|
|
415
|
-
nodes: /* @__PURE__ */ new Set(),
|
|
416
|
-
forwardChain: /* @__PURE__ */ new Map(),
|
|
417
|
-
reverseChain: /* @__PURE__ */ new Map(),
|
|
418
|
-
migrationByHash: /* @__PURE__ */ new Map()
|
|
419
|
-
},
|
|
420
|
-
packagesByMigrationHash: /* @__PURE__ */ new Map()
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
//#endregion
|
|
425
|
-
export { createMongoFamilyInstance, mongoFamilyDescriptor, mongoTargetDescriptor };
|
|
406
|
+
export { canonicalizeSchemasForVerification, contractToMongoSchemaIR, createMongoFamilyInstance, diffMongoSchemas, formatMongoOperations, mongoFamilyDescriptor, mongoOperationsToPreview };
|
|
426
407
|
|
|
427
408
|
//# sourceMappingURL=control.mjs.map
|