@prisma-next/target-mongo 0.5.0-dev.3 → 0.5.0-dev.30
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 +2 -0
- package/dist/control.d.mts +40 -18
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +83 -84
- package/dist/control.mjs.map +1 -1
- package/dist/{migration-factories-gwi81C8u.mjs → migration-factories-IG0vjM_u.mjs} +3 -1
- package/dist/migration-factories-IG0vjM_u.mjs.map +1 -0
- package/dist/migration.d.mts +7 -1
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +1 -1
- package/dist/{op-factory-call-BjNAcPSF.d.mts → op-factory-call-CVgzmLJh.d.mts} +1 -1
- package/dist/{op-factory-call-BjNAcPSF.d.mts.map → op-factory-call-CVgzmLJh.d.mts.map} +1 -1
- package/dist/schema-verify.d.mts +22 -0
- package/dist/schema-verify.d.mts.map +1 -0
- package/dist/schema-verify.mjs +3 -0
- package/dist/verify-mongo-schema-P0TRBJNs.mjs +582 -0
- package/dist/verify-mongo-schema-P0TRBJNs.mjs.map +1 -0
- package/package.json +18 -14
- package/src/core/marker-ledger.ts +90 -20
- package/src/core/migration-factories.ts +8 -0
- package/src/core/mongo-ops-serializer.ts +0 -8
- package/src/core/mongo-planner.ts +1 -1
- package/src/core/mongo-runner.ts +74 -42
- package/src/core/planner-produced-migration.ts +0 -1
- package/src/core/render-typescript.ts +3 -7
- 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/schema-verify.ts +2 -0
- package/dist/migration-factories-gwi81C8u.mjs.map +0 -1
package/dist/migration.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as dropCollection, c as validatedCollection, i as dataTransform, n as createCollection, o as dropIndex, r as createIndex, s as setValidation, t as collMod } from "./migration-factories-
|
|
1
|
+
import { a as dropCollection, c as validatedCollection, i as dataTransform, n as createCollection, o as dropIndex, r as createIndex, s as setValidation, t as collMod } from "./migration-factories-IG0vjM_u.mjs";
|
|
2
2
|
import { placeholder } from "@prisma-next/errors/migration";
|
|
3
3
|
|
|
4
4
|
export { collMod, createCollection, createIndex, dataTransform, dropCollection, dropIndex, placeholder, setValidation, validatedCollection };
|
|
@@ -74,4 +74,4 @@ declare function schemaIndexToCreateIndexOptions(index: MongoSchemaIndex): Creat
|
|
|
74
74
|
declare function schemaCollectionToCreateCollectionOptions(coll: MongoSchemaCollection): CreateCollectionOptions | undefined;
|
|
75
75
|
//#endregion
|
|
76
76
|
export { DropCollectionCall as a, schemaCollectionToCreateCollectionOptions as c, CreateIndexCall as i, schemaIndexToCreateIndexOptions as l, CollModMeta as n, DropIndexCall as o, CreateCollectionCall as r, OpFactoryCall$1 as s, CollModCall as t };
|
|
77
|
-
//# sourceMappingURL=op-factory-call-
|
|
77
|
+
//# sourceMappingURL=op-factory-call-CVgzmLJh.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"op-factory-call-
|
|
1
|
+
{"version":3,"file":"op-factory-call-CVgzmLJh.d.mts","names":[],"sources":["../src/core/op-factory-call.ts"],"sourcesContent":[],"mappings":";;;;;;;AAwEqC,UA3BpB,WAAA,CA2BoB;EAAiB,SAAA,EAAA,CAAA,EAAA,MAAA;EAgCzC,SAAA,KAAA,CAAA,EAAc,MAAA;EAII,SAAA,cAAA,CAAA,EA5DH,uBA4DG;;uBAvDhB,iBAAA,SAA0B,YAAA,YAAwB,aA0DX,CAAA;EAAd,kBAAA,WAAA,EAAA,MAAA;EAQ9B,kBAAA,cAAA,EAhE0B,uBAgE1B;EAfyB,kBAAA,KAAA,EAAA,MAAA;EAAiB,SAAA,IAAA,CAAA,CAAA,EA/CjC,2BA+CiC;EAwBvC,kBAAA,CAAA,CAAA,EAAA,SArEoB,iBAqEC,EAAA;EAId,UAAA,MAAA,CAAA,CAAA,EAAA,IAAA;;AAWV,cAvEG,eAAA,SAAwB,iBAAA,CAuE3B;EAfgC,SAAA,WAAA,EAAA,aAAA;EAAiB,SAAA,cAAA,EAAA,UAAA;EA0B9C,SAAA,UAAA,EAAmB,MAAA;EAsBnB,SAAA,IAAA,EApGI,aAoGQ,CApGM,aAoGN,CAAA;EAGL,SAAA,OAAA,EAtGA,kBAsGA,GAAA,SAAA;EACH,SAAA,KAAA,EAAA,MAAA;EACU,WAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EAnGjB,aAmGiB,CAnGH,aAmGG,CAAA,EAAA,OAAA,CAAA,EAlGb,kBAkGa;EAGgB,IAAA,CAAA,CAAA,EA3FjC,2BA2FiC;EAAuB,gBAAA,CAAA,CAAA,EAAA,MAAA;;AARjC,cAxEpB,aAAA,SAAsB,iBAAA,CAwEF;EAAiB,SAAA,WAAA,EAAA,WAAA;EA6BtC,SAAA,cAAa,EAAA,aAAA;EACrB,SAAA,UAAA,EAAA,MAAA;EACA,SAAA,IAAA,EAnGa,aAmGb,CAnG2B,aAmG3B,CAAA;EACA,SAAA,KAAA,EAAA,MAAA;EACA,WAAA,CAAA,UAAA,EAAA,MAAA,EAAA,IAAA,EAlGoC,aAkGpC,CAlGkD,aAkGlD,CAAA;EACA,IAAA,CAAA,CAAA,EA3FM,2BA2FN;EAAW,gBAAA,CAAA,CAAA,EAAA,MAAA;AAEf;AAcgB,cAlGH,oBAAA,SAA6B,iBAAA,CAmGlC;;;;oBA/FY;;4CAGwB;UAQlC;;;cAWG,kBAAA,SAA2B,iBAAA;;;;;;UAa9B;;;cASG,WAAA,SAAoB,iBAAA;;;oBAGb;iBACH;2BACU;;2CAGgB,uBAAuB;UAUxD;;;KAWE,eAAA,GACR,kBACA,gBACA,uBACA,qBACA;iBAEY,+BAAA,QAAuC,mBAAmB;iBAc1D,yCAAA,OACR,wBACL"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MongoSchemaIR } from "@prisma-next/mongo-schema-ir";
|
|
2
|
+
import { OperationContext, VerifyDatabaseSchemaResult } from "@prisma-next/framework-components/control";
|
|
3
|
+
import { MongoContract } from "@prisma-next/mongo-contract";
|
|
4
|
+
import { TargetBoundComponentDescriptor } from "@prisma-next/framework-components/components";
|
|
5
|
+
|
|
6
|
+
//#region src/core/schema-verify/verify-mongo-schema.d.ts
|
|
7
|
+
interface VerifyMongoSchemaOptions {
|
|
8
|
+
readonly contract: MongoContract;
|
|
9
|
+
readonly schema: MongoSchemaIR;
|
|
10
|
+
readonly strict: boolean;
|
|
11
|
+
readonly context?: OperationContext;
|
|
12
|
+
/**
|
|
13
|
+
* Active framework components participating in this composition. Mongo
|
|
14
|
+
* verification does not currently consult them, but the parameter exists
|
|
15
|
+
* for parity with `verifySqlSchema` so callers can pass the same envelope.
|
|
16
|
+
*/
|
|
17
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'mongo', string>>;
|
|
18
|
+
}
|
|
19
|
+
declare function verifyMongoSchema(options: VerifyMongoSchemaOptions): VerifyDatabaseSchemaResult;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { type VerifyMongoSchemaOptions, verifyMongoSchema };
|
|
22
|
+
//# sourceMappingURL=schema-verify.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-verify.d.mts","names":[],"sources":["../src/core/schema-verify/verify-mongo-schema.ts"],"sourcesContent":[],"mappings":";;;;;;UAaiB,wBAAA;qBACI;EADJ,SAAA,MAAA,EAEE,aAFsB;EACpB,SAAA,MAAA,EAAA,OAAA;EACF,SAAA,OAAA,CAAA,EAEE,gBAFF;EAEE;;;;AASrB;gCAHgC,cAAc;;iBAG9B,iBAAA,UAA2B,2BAA2B"}
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { MongoSchemaCollection, MongoSchemaCollectionOptions, MongoSchemaIR, MongoSchemaIndex, MongoSchemaValidator, canonicalize, deepEqual } from "@prisma-next/mongo-schema-ir";
|
|
2
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
3
|
+
import { VERIFY_CODE_SCHEMA_FAILURE } from "@prisma-next/framework-components/control";
|
|
4
|
+
|
|
5
|
+
//#region src/core/contract-to-schema.ts
|
|
6
|
+
function convertIndex(index) {
|
|
7
|
+
return new MongoSchemaIndex({
|
|
8
|
+
keys: index.keys,
|
|
9
|
+
unique: index.unique,
|
|
10
|
+
sparse: index.sparse,
|
|
11
|
+
expireAfterSeconds: index.expireAfterSeconds,
|
|
12
|
+
partialFilterExpression: index.partialFilterExpression,
|
|
13
|
+
wildcardProjection: index.wildcardProjection,
|
|
14
|
+
collation: index.collation,
|
|
15
|
+
weights: index.weights,
|
|
16
|
+
default_language: index.default_language,
|
|
17
|
+
language_override: index.language_override
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function convertValidator(v) {
|
|
21
|
+
return new MongoSchemaValidator({
|
|
22
|
+
jsonSchema: v.jsonSchema,
|
|
23
|
+
validationLevel: v.validationLevel,
|
|
24
|
+
validationAction: v.validationAction
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function convertOptions(o) {
|
|
28
|
+
return new MongoSchemaCollectionOptions(o);
|
|
29
|
+
}
|
|
30
|
+
function convertCollection(name, def) {
|
|
31
|
+
return new MongoSchemaCollection({
|
|
32
|
+
name,
|
|
33
|
+
indexes: (def.indexes ?? []).map(convertIndex),
|
|
34
|
+
...def.validator != null && { validator: convertValidator(def.validator) },
|
|
35
|
+
...def.options != null && { options: convertOptions(def.options) }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function contractToMongoSchemaIR(contract) {
|
|
39
|
+
if (!contract) return new MongoSchemaIR([]);
|
|
40
|
+
return new MongoSchemaIR(Object.entries(contract.storage.collections).map(([name, def]) => convertCollection(name, def)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/core/schema-diff.ts
|
|
45
|
+
function diffMongoSchemas(live, expected, strict) {
|
|
46
|
+
const issues = [];
|
|
47
|
+
const collectionChildren = [];
|
|
48
|
+
let pass = 0;
|
|
49
|
+
let warn = 0;
|
|
50
|
+
let fail = 0;
|
|
51
|
+
const allNames = new Set([...live.collectionNames, ...expected.collectionNames]);
|
|
52
|
+
for (const name of [...allNames].sort()) {
|
|
53
|
+
const liveColl = live.collection(name);
|
|
54
|
+
const expectedColl = expected.collection(name);
|
|
55
|
+
if (!liveColl && expectedColl) {
|
|
56
|
+
issues.push({
|
|
57
|
+
kind: "missing_table",
|
|
58
|
+
table: name,
|
|
59
|
+
message: `Collection "${name}" is missing from the database`
|
|
60
|
+
});
|
|
61
|
+
collectionChildren.push({
|
|
62
|
+
status: "fail",
|
|
63
|
+
kind: "collection",
|
|
64
|
+
name,
|
|
65
|
+
contractPath: `storage.collections.${name}`,
|
|
66
|
+
code: "MISSING_COLLECTION",
|
|
67
|
+
message: `Collection "${name}" is missing`,
|
|
68
|
+
expected: name,
|
|
69
|
+
actual: null,
|
|
70
|
+
children: []
|
|
71
|
+
});
|
|
72
|
+
fail++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (liveColl && !expectedColl) {
|
|
76
|
+
const status = strict ? "fail" : "warn";
|
|
77
|
+
issues.push({
|
|
78
|
+
kind: "extra_table",
|
|
79
|
+
table: name,
|
|
80
|
+
message: `Extra collection "${name}" exists in the database but not in the contract`
|
|
81
|
+
});
|
|
82
|
+
collectionChildren.push({
|
|
83
|
+
status,
|
|
84
|
+
kind: "collection",
|
|
85
|
+
name,
|
|
86
|
+
contractPath: `storage.collections.${name}`,
|
|
87
|
+
code: "EXTRA_COLLECTION",
|
|
88
|
+
message: `Extra collection "${name}" found`,
|
|
89
|
+
expected: null,
|
|
90
|
+
actual: name,
|
|
91
|
+
children: []
|
|
92
|
+
});
|
|
93
|
+
if (status === "fail") fail++;
|
|
94
|
+
else warn++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const lc = liveColl;
|
|
98
|
+
const ec = expectedColl;
|
|
99
|
+
const indexChildren = diffIndexes(name, lc, ec, strict, issues);
|
|
100
|
+
const validatorChildren = diffValidator(name, lc, ec, strict, issues);
|
|
101
|
+
const optionsChildren = diffOptions(name, lc, ec, strict, issues);
|
|
102
|
+
const children = [
|
|
103
|
+
...indexChildren,
|
|
104
|
+
...validatorChildren,
|
|
105
|
+
...optionsChildren
|
|
106
|
+
];
|
|
107
|
+
const worstStatus = children.reduce((s, c) => c.status === "fail" ? "fail" : c.status === "warn" && s !== "fail" ? "warn" : s, "pass");
|
|
108
|
+
for (const c of children) if (c.status === "pass") pass++;
|
|
109
|
+
else if (c.status === "warn") warn++;
|
|
110
|
+
else fail++;
|
|
111
|
+
if (children.length === 0) pass++;
|
|
112
|
+
collectionChildren.push({
|
|
113
|
+
status: worstStatus,
|
|
114
|
+
kind: "collection",
|
|
115
|
+
name,
|
|
116
|
+
contractPath: `storage.collections.${name}`,
|
|
117
|
+
code: worstStatus === "pass" ? "MATCH" : "DRIFT",
|
|
118
|
+
message: worstStatus === "pass" ? `Collection "${name}" matches` : `Collection "${name}" has drift`,
|
|
119
|
+
expected: name,
|
|
120
|
+
actual: name,
|
|
121
|
+
children
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const rootStatus = fail > 0 ? "fail" : warn > 0 ? "warn" : "pass";
|
|
125
|
+
const totalNodes = pass + warn + fail + collectionChildren.length;
|
|
126
|
+
return {
|
|
127
|
+
root: {
|
|
128
|
+
status: rootStatus,
|
|
129
|
+
kind: "root",
|
|
130
|
+
name: "mongo-schema",
|
|
131
|
+
contractPath: "storage",
|
|
132
|
+
code: rootStatus === "pass" ? "MATCH" : "DRIFT",
|
|
133
|
+
message: rootStatus === "pass" ? "Schema matches" : "Schema has drift",
|
|
134
|
+
expected: null,
|
|
135
|
+
actual: null,
|
|
136
|
+
children: collectionChildren
|
|
137
|
+
},
|
|
138
|
+
issues,
|
|
139
|
+
counts: {
|
|
140
|
+
pass,
|
|
141
|
+
warn,
|
|
142
|
+
fail,
|
|
143
|
+
totalNodes
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function buildIndexLookupKey(index) {
|
|
148
|
+
const keys = index.keys.map((k) => `${k.field}:${k.direction}`).join(",");
|
|
149
|
+
const opts = [
|
|
150
|
+
index.unique ? "unique" : "",
|
|
151
|
+
index.sparse ? "sparse" : "",
|
|
152
|
+
index.expireAfterSeconds != null ? `ttl:${index.expireAfterSeconds}` : "",
|
|
153
|
+
index.partialFilterExpression ? `pfe:${canonicalize(index.partialFilterExpression)}` : "",
|
|
154
|
+
index.wildcardProjection ? `wp:${canonicalize(index.wildcardProjection)}` : "",
|
|
155
|
+
index.collation ? `col:${canonicalize(index.collation)}` : "",
|
|
156
|
+
index.weights ? `wt:${canonicalize(index.weights)}` : "",
|
|
157
|
+
index.default_language ? `dl:${index.default_language}` : "",
|
|
158
|
+
index.language_override ? `lo:${index.language_override}` : ""
|
|
159
|
+
].filter(Boolean).join(";");
|
|
160
|
+
return opts ? `${keys}|${opts}` : keys;
|
|
161
|
+
}
|
|
162
|
+
function formatIndexName(index) {
|
|
163
|
+
return index.keys.map((k) => `${k.field}:${k.direction}`).join(", ");
|
|
164
|
+
}
|
|
165
|
+
function diffIndexes(collName, live, expected, strict, issues) {
|
|
166
|
+
const nodes = [];
|
|
167
|
+
const liveLookup = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const idx of live.indexes) liveLookup.set(buildIndexLookupKey(idx), idx);
|
|
169
|
+
const expectedLookup = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const idx of expected.indexes) expectedLookup.set(buildIndexLookupKey(idx), idx);
|
|
171
|
+
for (const [key, idx] of expectedLookup) if (liveLookup.has(key)) nodes.push({
|
|
172
|
+
status: "pass",
|
|
173
|
+
kind: "index",
|
|
174
|
+
name: formatIndexName(idx),
|
|
175
|
+
contractPath: `storage.collections.${collName}.indexes`,
|
|
176
|
+
code: "MATCH",
|
|
177
|
+
message: `Index ${formatIndexName(idx)} matches`,
|
|
178
|
+
expected: key,
|
|
179
|
+
actual: key,
|
|
180
|
+
children: []
|
|
181
|
+
});
|
|
182
|
+
else {
|
|
183
|
+
issues.push({
|
|
184
|
+
kind: "index_mismatch",
|
|
185
|
+
table: collName,
|
|
186
|
+
indexOrConstraint: formatIndexName(idx),
|
|
187
|
+
message: `Index ${formatIndexName(idx)} missing on collection "${collName}"`
|
|
188
|
+
});
|
|
189
|
+
nodes.push({
|
|
190
|
+
status: "fail",
|
|
191
|
+
kind: "index",
|
|
192
|
+
name: formatIndexName(idx),
|
|
193
|
+
contractPath: `storage.collections.${collName}.indexes`,
|
|
194
|
+
code: "MISSING_INDEX",
|
|
195
|
+
message: `Index ${formatIndexName(idx)} missing`,
|
|
196
|
+
expected: key,
|
|
197
|
+
actual: null,
|
|
198
|
+
children: []
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
for (const [key, idx] of liveLookup) if (!expectedLookup.has(key)) {
|
|
202
|
+
const status = strict ? "fail" : "warn";
|
|
203
|
+
issues.push({
|
|
204
|
+
kind: "extra_index",
|
|
205
|
+
table: collName,
|
|
206
|
+
indexOrConstraint: formatIndexName(idx),
|
|
207
|
+
message: `Extra index ${formatIndexName(idx)} on collection "${collName}"`
|
|
208
|
+
});
|
|
209
|
+
nodes.push({
|
|
210
|
+
status,
|
|
211
|
+
kind: "index",
|
|
212
|
+
name: formatIndexName(idx),
|
|
213
|
+
contractPath: `storage.collections.${collName}.indexes`,
|
|
214
|
+
code: "EXTRA_INDEX",
|
|
215
|
+
message: `Extra index ${formatIndexName(idx)}`,
|
|
216
|
+
expected: null,
|
|
217
|
+
actual: key,
|
|
218
|
+
children: []
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return nodes;
|
|
222
|
+
}
|
|
223
|
+
function diffValidator(collName, live, expected, strict, issues) {
|
|
224
|
+
if (!live.validator && !expected.validator) return [];
|
|
225
|
+
if (expected.validator && !live.validator) {
|
|
226
|
+
issues.push({
|
|
227
|
+
kind: "type_missing",
|
|
228
|
+
table: collName,
|
|
229
|
+
message: `Validator missing on collection "${collName}"`
|
|
230
|
+
});
|
|
231
|
+
return [{
|
|
232
|
+
status: "fail",
|
|
233
|
+
kind: "validator",
|
|
234
|
+
name: "validator",
|
|
235
|
+
contractPath: `storage.collections.${collName}.validator`,
|
|
236
|
+
code: "MISSING_VALIDATOR",
|
|
237
|
+
message: "Validator missing",
|
|
238
|
+
expected: canonicalize(expected.validator.jsonSchema),
|
|
239
|
+
actual: null,
|
|
240
|
+
children: []
|
|
241
|
+
}];
|
|
242
|
+
}
|
|
243
|
+
if (!expected.validator && live.validator) {
|
|
244
|
+
const status = strict ? "fail" : "warn";
|
|
245
|
+
issues.push({
|
|
246
|
+
kind: "extra_validator",
|
|
247
|
+
table: collName,
|
|
248
|
+
message: `Extra validator on collection "${collName}"`
|
|
249
|
+
});
|
|
250
|
+
return [{
|
|
251
|
+
status,
|
|
252
|
+
kind: "validator",
|
|
253
|
+
name: "validator",
|
|
254
|
+
contractPath: `storage.collections.${collName}.validator`,
|
|
255
|
+
code: "EXTRA_VALIDATOR",
|
|
256
|
+
message: "Extra validator found",
|
|
257
|
+
expected: null,
|
|
258
|
+
actual: canonicalize(live.validator.jsonSchema),
|
|
259
|
+
children: []
|
|
260
|
+
}];
|
|
261
|
+
}
|
|
262
|
+
const liveVal = live.validator;
|
|
263
|
+
const expectedVal = expected.validator;
|
|
264
|
+
const liveSchema = canonicalize(liveVal.jsonSchema);
|
|
265
|
+
const expectedSchema = canonicalize(expectedVal.jsonSchema);
|
|
266
|
+
if (liveSchema !== expectedSchema || liveVal.validationLevel !== expectedVal.validationLevel || liveVal.validationAction !== expectedVal.validationAction) {
|
|
267
|
+
issues.push({
|
|
268
|
+
kind: "type_mismatch",
|
|
269
|
+
table: collName,
|
|
270
|
+
expected: expectedSchema,
|
|
271
|
+
actual: liveSchema,
|
|
272
|
+
message: `Validator mismatch on collection "${collName}"`
|
|
273
|
+
});
|
|
274
|
+
return [{
|
|
275
|
+
status: "fail",
|
|
276
|
+
kind: "validator",
|
|
277
|
+
name: "validator",
|
|
278
|
+
contractPath: `storage.collections.${collName}.validator`,
|
|
279
|
+
code: "VALIDATOR_MISMATCH",
|
|
280
|
+
message: "Validator mismatch",
|
|
281
|
+
expected: {
|
|
282
|
+
jsonSchema: expectedVal.jsonSchema,
|
|
283
|
+
validationLevel: expectedVal.validationLevel,
|
|
284
|
+
validationAction: expectedVal.validationAction
|
|
285
|
+
},
|
|
286
|
+
actual: {
|
|
287
|
+
jsonSchema: liveVal.jsonSchema,
|
|
288
|
+
validationLevel: liveVal.validationLevel,
|
|
289
|
+
validationAction: liveVal.validationAction
|
|
290
|
+
},
|
|
291
|
+
children: []
|
|
292
|
+
}];
|
|
293
|
+
}
|
|
294
|
+
return [{
|
|
295
|
+
status: "pass",
|
|
296
|
+
kind: "validator",
|
|
297
|
+
name: "validator",
|
|
298
|
+
contractPath: `storage.collections.${collName}.validator`,
|
|
299
|
+
code: "MATCH",
|
|
300
|
+
message: "Validator matches",
|
|
301
|
+
expected: expectedSchema,
|
|
302
|
+
actual: liveSchema,
|
|
303
|
+
children: []
|
|
304
|
+
}];
|
|
305
|
+
}
|
|
306
|
+
function diffOptions(collName, live, expected, strict, issues) {
|
|
307
|
+
if (!live.options && !expected.options) return [];
|
|
308
|
+
if (!expected.options && live.options) {
|
|
309
|
+
const status = strict ? "fail" : "warn";
|
|
310
|
+
issues.push({
|
|
311
|
+
kind: "type_mismatch",
|
|
312
|
+
table: collName,
|
|
313
|
+
actual: canonicalize(live.options),
|
|
314
|
+
message: `Extra collection options on "${collName}"`
|
|
315
|
+
});
|
|
316
|
+
return [{
|
|
317
|
+
status,
|
|
318
|
+
kind: "options",
|
|
319
|
+
name: "options",
|
|
320
|
+
contractPath: `storage.collections.${collName}.options`,
|
|
321
|
+
code: "EXTRA_OPTIONS",
|
|
322
|
+
message: "Extra collection options found",
|
|
323
|
+
expected: null,
|
|
324
|
+
actual: live.options,
|
|
325
|
+
children: []
|
|
326
|
+
}];
|
|
327
|
+
}
|
|
328
|
+
if (deepEqual(live.options, expected.options)) return [{
|
|
329
|
+
status: "pass",
|
|
330
|
+
kind: "options",
|
|
331
|
+
name: "options",
|
|
332
|
+
contractPath: `storage.collections.${collName}.options`,
|
|
333
|
+
code: "MATCH",
|
|
334
|
+
message: "Collection options match",
|
|
335
|
+
expected: canonicalize(expected.options),
|
|
336
|
+
actual: canonicalize(live.options),
|
|
337
|
+
children: []
|
|
338
|
+
}];
|
|
339
|
+
issues.push({
|
|
340
|
+
kind: "type_mismatch",
|
|
341
|
+
table: collName,
|
|
342
|
+
expected: canonicalize(expected.options),
|
|
343
|
+
actual: canonicalize(live.options),
|
|
344
|
+
message: `Collection options mismatch on "${collName}"`
|
|
345
|
+
});
|
|
346
|
+
return [{
|
|
347
|
+
status: "fail",
|
|
348
|
+
kind: "options",
|
|
349
|
+
name: "options",
|
|
350
|
+
contractPath: `storage.collections.${collName}.options`,
|
|
351
|
+
code: "OPTIONS_MISMATCH",
|
|
352
|
+
message: "Collection options mismatch",
|
|
353
|
+
expected: expected.options,
|
|
354
|
+
actual: live.options,
|
|
355
|
+
children: []
|
|
356
|
+
}];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/core/schema-verify/canonicalize-introspection.ts
|
|
361
|
+
function canonicalizeSchemasForVerification(live, expected) {
|
|
362
|
+
const expectedByName = /* @__PURE__ */ new Map();
|
|
363
|
+
for (const c of expected.collections) expectedByName.set(c.name, c);
|
|
364
|
+
const liveByName = /* @__PURE__ */ new Map();
|
|
365
|
+
for (const c of live.collections) liveByName.set(c.name, c);
|
|
366
|
+
const canonicalLive = live.collections.map((c) => canonicalizeLiveCollection(c, expectedByName.get(c.name)));
|
|
367
|
+
const canonicalExpected = expected.collections.map((c) => canonicalizeExpectedCollection(c, liveByName.get(c.name)));
|
|
368
|
+
return {
|
|
369
|
+
live: new MongoSchemaIR(canonicalLive),
|
|
370
|
+
expected: new MongoSchemaIR(canonicalExpected)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function canonicalizeLiveCollection(liveColl, expectedColl) {
|
|
374
|
+
const expectedIndexes = expectedColl?.indexes ?? [];
|
|
375
|
+
const indexes = liveColl.indexes.map((idx) => canonicalizeLiveIndex(idx, findExpectedIndexCounterpart(idx, expectedIndexes)));
|
|
376
|
+
const options = liveColl.options ? canonicalizeLiveOptions(liveColl.options, expectedColl?.options) : void 0;
|
|
377
|
+
return new MongoSchemaCollection({
|
|
378
|
+
name: liveColl.name,
|
|
379
|
+
indexes,
|
|
380
|
+
...ifDefined("validator", liveColl.validator),
|
|
381
|
+
...ifDefined("options", options)
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function canonicalizeExpectedCollection(expectedColl, liveColl) {
|
|
385
|
+
const indexes = expectedColl.indexes.map(canonicalizeTextIndexKeyOrder);
|
|
386
|
+
const options = expectedColl.options ? canonicalizeExpectedOptions(expectedColl.options, liveColl?.options) : void 0;
|
|
387
|
+
return new MongoSchemaCollection({
|
|
388
|
+
name: expectedColl.name,
|
|
389
|
+
indexes,
|
|
390
|
+
...ifDefined("validator", expectedColl.validator),
|
|
391
|
+
...ifDefined("options", options)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function canonicalizeTextIndexKeyOrder(index) {
|
|
395
|
+
if (!index.keys.some((k) => k.direction === "text")) return index;
|
|
396
|
+
return new MongoSchemaIndex({
|
|
397
|
+
keys: sortTextKeys(index.keys),
|
|
398
|
+
unique: index.unique,
|
|
399
|
+
...ifDefined("sparse", index.sparse),
|
|
400
|
+
...ifDefined("expireAfterSeconds", index.expireAfterSeconds),
|
|
401
|
+
...ifDefined("partialFilterExpression", index.partialFilterExpression),
|
|
402
|
+
...ifDefined("wildcardProjection", index.wildcardProjection),
|
|
403
|
+
...ifDefined("collation", index.collation),
|
|
404
|
+
...ifDefined("weights", index.weights),
|
|
405
|
+
...ifDefined("default_language", index.default_language),
|
|
406
|
+
...ifDefined("language_override", index.language_override)
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Returns a copy of `keys` with text-direction entries sorted alphabetically
|
|
411
|
+
* while preserving the relative position of non-text entries. Compound text
|
|
412
|
+
* indexes (`{a: 1, _fts: 'text', _ftsx: 1, b: 1}`) keep their scalar
|
|
413
|
+
* prefix/suffix layout; only the contiguous text block is reordered.
|
|
414
|
+
*/
|
|
415
|
+
function sortTextKeys(keys) {
|
|
416
|
+
const textEntries = keys.filter((k) => k.direction === "text");
|
|
417
|
+
if (textEntries.length <= 1) return keys;
|
|
418
|
+
const sortedText = [...textEntries].sort((a, b) => a.field.localeCompare(b.field));
|
|
419
|
+
let textIdx = 0;
|
|
420
|
+
return keys.map((k) => {
|
|
421
|
+
if (k.direction !== "text") return k;
|
|
422
|
+
const next = sortedText[textIdx++];
|
|
423
|
+
/* v8 ignore next 3 -- @preserve invariant guard: textIdx is always < sortedText.length here because we only consume sortedText for text-direction entries and sortedText is built from the same filter. */
|
|
424
|
+
if (next === void 0) throw new Error("sortTextKeys: text-key counts mismatched");
|
|
425
|
+
return next;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
function canonicalizeLiveIndex(liveIndex, expectedIndex) {
|
|
429
|
+
const projectedKeys = sortTextKeys(projectTextIndexKeys(liveIndex));
|
|
430
|
+
const collation = liveIndex.collation ? stripUnspecifiedFields(liveIndex.collation, expectedIndex?.collation) : liveIndex.collation;
|
|
431
|
+
const weights = expectedIndex?.weights === void 0 && hasDefaultTextWeights(projectedKeys, liveIndex.weights) ? void 0 : liveIndex.weights;
|
|
432
|
+
const default_language = expectedIndex?.default_language === void 0 && liveIndex.default_language === "english" ? void 0 : liveIndex.default_language;
|
|
433
|
+
const language_override = expectedIndex?.language_override === void 0 && liveIndex.language_override === "language" ? void 0 : liveIndex.language_override;
|
|
434
|
+
return new MongoSchemaIndex({
|
|
435
|
+
keys: projectedKeys,
|
|
436
|
+
unique: liveIndex.unique,
|
|
437
|
+
...ifDefined("sparse", liveIndex.sparse),
|
|
438
|
+
...ifDefined("expireAfterSeconds", liveIndex.expireAfterSeconds),
|
|
439
|
+
...ifDefined("partialFilterExpression", liveIndex.partialFilterExpression),
|
|
440
|
+
...ifDefined("wildcardProjection", liveIndex.wildcardProjection),
|
|
441
|
+
...ifDefined("collation", collation),
|
|
442
|
+
...ifDefined("weights", weights),
|
|
443
|
+
...ifDefined("default_language", default_language),
|
|
444
|
+
...ifDefined("language_override", language_override)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Locate the contract-side index that corresponds to a live index for the
|
|
449
|
+
* purpose of contract-aware normalization. We deliberately match by the
|
|
450
|
+
* *projected* (contract-shaped) key list — so a live `_fts/_ftsx` index
|
|
451
|
+
* resolves to a contract `{title: 'text', body: 'text'}` index — and pick
|
|
452
|
+
* the first match. Contracts very rarely contain duplicate-key indexes; if
|
|
453
|
+
* we have no counterpart we fall back to no normalization for that index.
|
|
454
|
+
*/
|
|
455
|
+
function findExpectedIndexCounterpart(liveIndex, expectedIndexes) {
|
|
456
|
+
const liveKeySig = sortTextKeys(projectTextIndexKeys(liveIndex)).map((k) => `${k.field}:${k.direction}`).join(",");
|
|
457
|
+
for (const expected of expectedIndexes) if (sortTextKeys(expected.keys).map((k) => `${k.field}:${k.direction}`).join(",") === liveKeySig) return expected;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* MongoDB expands a contract-shaped text index like
|
|
461
|
+
* `[{title: 'text'}, {body: 'text'}]` into its internal weighted vector
|
|
462
|
+
* representation `[{_fts: 'text'}, {_ftsx: 1}]`. We project back to
|
|
463
|
+
* contract-shaped keys via `weights`, iterating in whatever order MongoDB
|
|
464
|
+
* returns them (alphabetical, in practice). `sortTextKeys` is applied
|
|
465
|
+
* downstream to canonicalize the order on both sides, so this projection
|
|
466
|
+
* does not depend on a specific iteration order.
|
|
467
|
+
*/
|
|
468
|
+
function projectTextIndexKeys(liveIndex) {
|
|
469
|
+
if (!(liveIndex.keys.length >= 1 && liveIndex.keys.some((k) => k.field === "_fts" && k.direction === "text")) || !liveIndex.weights) return liveIndex.keys;
|
|
470
|
+
const textKeys = Object.keys(liveIndex.weights).map((field) => ({
|
|
471
|
+
field,
|
|
472
|
+
direction: "text"
|
|
473
|
+
}));
|
|
474
|
+
const projectedKeys = [];
|
|
475
|
+
for (const key of liveIndex.keys) {
|
|
476
|
+
if (key.field === "_ftsx") continue;
|
|
477
|
+
if (key.field === "_fts") {
|
|
478
|
+
projectedKeys.push(...textKeys);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
projectedKeys.push(key);
|
|
482
|
+
}
|
|
483
|
+
return projectedKeys;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* MongoDB's server-default `weights` for an authored-without-weights text
|
|
487
|
+
* index assigns `1` to every text-direction field. Returns `true` only when
|
|
488
|
+
* `liveWeights` is exactly that uniform shape (every projected text-direction
|
|
489
|
+
* key weighted at `1`) so the canonicalizer leaves non-default weights —
|
|
490
|
+
* including out-of-band relevance tweaks — visible to the verifier.
|
|
491
|
+
*
|
|
492
|
+
* `projectTextIndexKeys` derives text-direction keys from the live weights
|
|
493
|
+
* map, so the count is guaranteed to match; we only have to check the value
|
|
494
|
+
* shape.
|
|
495
|
+
*/
|
|
496
|
+
function hasDefaultTextWeights(projectedKeys, liveWeights) {
|
|
497
|
+
if (liveWeights === void 0) return false;
|
|
498
|
+
return projectedKeys.filter((k) => k.direction === "text").map((k) => k.field).every((field) => liveWeights[field] === 1);
|
|
499
|
+
}
|
|
500
|
+
function canonicalizeLiveOptions(liveOptions, expectedOptions) {
|
|
501
|
+
const collation = liveOptions.collation ? stripUnspecifiedFields(liveOptions.collation, expectedOptions?.collation) : void 0;
|
|
502
|
+
const timeseries = liveOptions.timeseries ? stripUnspecifiedFields(liveOptions.timeseries, expectedOptions?.timeseries) : void 0;
|
|
503
|
+
const clusteredIndex = liveOptions.clusteredIndex ? stripUnspecifiedFields(liveOptions.clusteredIndex, expectedOptions?.clusteredIndex) : void 0;
|
|
504
|
+
const changeStreamPreAndPostImages = isDisabledChangeStream(liveOptions.changeStreamPreAndPostImages) ? void 0 : liveOptions.changeStreamPreAndPostImages;
|
|
505
|
+
if (!(liveOptions.capped || timeseries || collation || changeStreamPreAndPostImages || clusteredIndex)) return void 0;
|
|
506
|
+
return new MongoSchemaCollectionOptions({
|
|
507
|
+
...ifDefined("capped", liveOptions.capped),
|
|
508
|
+
...ifDefined("timeseries", timeseries),
|
|
509
|
+
...ifDefined("collation", collation),
|
|
510
|
+
...ifDefined("changeStreamPreAndPostImages", changeStreamPreAndPostImages),
|
|
511
|
+
...ifDefined("clusteredIndex", clusteredIndex)
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function canonicalizeExpectedOptions(expectedOptions, _liveOptions) {
|
|
515
|
+
const changeStreamPreAndPostImages = isDisabledChangeStream(expectedOptions.changeStreamPreAndPostImages) ? void 0 : expectedOptions.changeStreamPreAndPostImages;
|
|
516
|
+
if (!(expectedOptions.capped || expectedOptions.timeseries || expectedOptions.collation || changeStreamPreAndPostImages || expectedOptions.clusteredIndex)) return void 0;
|
|
517
|
+
return new MongoSchemaCollectionOptions({
|
|
518
|
+
...ifDefined("capped", expectedOptions.capped),
|
|
519
|
+
...ifDefined("timeseries", expectedOptions.timeseries),
|
|
520
|
+
...ifDefined("collation", expectedOptions.collation),
|
|
521
|
+
...ifDefined("changeStreamPreAndPostImages", changeStreamPreAndPostImages),
|
|
522
|
+
...ifDefined("clusteredIndex", expectedOptions.clusteredIndex)
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function isDisabledChangeStream(value) {
|
|
526
|
+
return value !== void 0 && value.enabled === false;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Returns a copy of `live` containing only the keys that `expected` defines.
|
|
530
|
+
* Used for option families whose individual sub-fields are server-extended
|
|
531
|
+
* with platform defaults (collation, timeseries, clusteredIndex), so the
|
|
532
|
+
* verifier should compare only what the contract actually authored.
|
|
533
|
+
*
|
|
534
|
+
* When `expected` is `undefined` — i.e. the contract authored nothing for
|
|
535
|
+
* this whole option family but the live IR has it — we return `live`
|
|
536
|
+
* unchanged so the verifier still sees the entire live block and can
|
|
537
|
+
* surface it as drift. (Returning `undefined` here would silently strip a
|
|
538
|
+
* server-attached collation/timeseries/clusteredIndex that the contract
|
|
539
|
+
* never asked for, hiding real drift.)
|
|
540
|
+
*/
|
|
541
|
+
function stripUnspecifiedFields(live, expected) {
|
|
542
|
+
if (expected === void 0) return live;
|
|
543
|
+
const out = {};
|
|
544
|
+
for (const key of Object.keys(expected)) if (Object.hasOwn(live, key)) out[key] = live[key];
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/core/schema-verify/verify-mongo-schema.ts
|
|
550
|
+
function verifyMongoSchema(options) {
|
|
551
|
+
const { contract, schema, strict, context } = options;
|
|
552
|
+
const startTime = Date.now();
|
|
553
|
+
const { live: canonicalLive, expected: canonicalExpected } = canonicalizeSchemasForVerification(schema, contractToMongoSchemaIR(contract));
|
|
554
|
+
const { root, issues, counts } = diffMongoSchemas(canonicalLive, canonicalExpected, strict);
|
|
555
|
+
const ok = counts.fail === 0;
|
|
556
|
+
const profileHash = typeof contract.profileHash === "string" ? contract.profileHash : "";
|
|
557
|
+
return {
|
|
558
|
+
ok,
|
|
559
|
+
...ifDefined("code", ok ? void 0 : VERIFY_CODE_SCHEMA_FAILURE),
|
|
560
|
+
summary: ok ? "Schema matches contract" : `Schema verification found ${counts.fail} issue(s)`,
|
|
561
|
+
contract: {
|
|
562
|
+
storageHash: contract.storage.storageHash,
|
|
563
|
+
...profileHash ? { profileHash } : {}
|
|
564
|
+
},
|
|
565
|
+
target: { expected: contract.target },
|
|
566
|
+
schema: {
|
|
567
|
+
issues,
|
|
568
|
+
root,
|
|
569
|
+
counts
|
|
570
|
+
},
|
|
571
|
+
meta: {
|
|
572
|
+
strict,
|
|
573
|
+
...ifDefined("contractPath", context?.contractPath),
|
|
574
|
+
...ifDefined("configPath", context?.configPath)
|
|
575
|
+
},
|
|
576
|
+
timings: { total: Date.now() - startTime }
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
//#endregion
|
|
581
|
+
export { contractToMongoSchemaIR as n, verifyMongoSchema as t };
|
|
582
|
+
//# sourceMappingURL=verify-mongo-schema-P0TRBJNs.mjs.map
|