@prisma-next/family-mongo 0.7.0 → 0.8.0-dev.10

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.
Files changed (37) hide show
  1. package/README.md +22 -9
  2. package/dist/control-adapter.d.mts +75 -0
  3. package/dist/control-adapter.d.mts.map +1 -0
  4. package/dist/control-adapter.mjs +1 -0
  5. package/dist/control.d.mts +60 -16
  6. package/dist/control.d.mts.map +1 -1
  7. package/dist/control.mjs +257 -276
  8. package/dist/control.mjs.map +1 -1
  9. package/dist/ir.d.mts +131 -0
  10. package/dist/ir.d.mts.map +1 -0
  11. package/dist/ir.mjs +54 -0
  12. package/dist/ir.mjs.map +1 -0
  13. package/dist/mongo-contract-serializer-Co3EaTVj.mjs +98 -0
  14. package/dist/mongo-contract-serializer-Co3EaTVj.mjs.map +1 -0
  15. package/dist/schema-verify.d.mts +22 -2
  16. package/dist/schema-verify.d.mts.map +1 -0
  17. package/dist/schema-verify.mjs +1 -1
  18. package/dist/verify-mongo-schema-BL7t9YTB.mjs +592 -0
  19. package/dist/verify-mongo-schema-BL7t9YTB.mjs.map +1 -0
  20. package/package.json +20 -22
  21. package/src/core/contract-to-schema.ts +84 -0
  22. package/src/core/control-adapter.ts +97 -0
  23. package/src/core/control-instance.ts +275 -272
  24. package/src/core/control-target-descriptor.ts +26 -0
  25. package/src/core/control-types.ts +6 -4
  26. package/src/core/ir/mongo-contract-serializer-base.ts +124 -0
  27. package/src/core/ir/mongo-contract-serializer.ts +18 -0
  28. package/src/core/ir/mongo-schema-verifier-base.ts +87 -0
  29. package/src/core/operation-preview.ts +131 -0
  30. package/src/core/schema-diff.ts +402 -0
  31. package/src/core/schema-verify/canonicalize-introspection.ts +389 -0
  32. package/src/core/schema-verify/verify-mongo-schema.ts +60 -0
  33. package/src/exports/control-adapter.ts +1 -0
  34. package/src/exports/control.ts +8 -1
  35. package/src/exports/ir.ts +3 -0
  36. package/src/exports/schema-verify.ts +4 -2
  37. package/src/core/mongo-target-descriptor.ts +0 -180
@@ -0,0 +1,124 @@
1
+ import { validateContractDomain } from '@prisma-next/contract/validate-domain';
2
+ import type { ContractSerializer } from '@prisma-next/framework-components/control';
3
+ import {
4
+ MongoCollection,
5
+ type MongoCollectionInput,
6
+ type MongoContract,
7
+ MongoContractSchema,
8
+ validateMongoStorage,
9
+ } from '@prisma-next/mongo-contract';
10
+ import type { JsonObject } from '@prisma-next/utils/json';
11
+ import { type as arktypeType } from 'arktype';
12
+
13
+ /**
14
+ * Mongo family `ContractSerializer` abstract base. Owns the family-shared
15
+ * deserialization pipeline:
16
+ *
17
+ * 1. Structural validation against the Mongo contract arktype schema
18
+ * (`MongoContractSchema`).
19
+ * 2. Framework-shared domain validation (`validateContractDomain`).
20
+ * 3. Family-shared storage validation (`validateMongoStorage`).
21
+ *
22
+ * The validated value is handed to the target via the
23
+ * `constructTargetContract` hook, which wraps the plain-JSON shape in
24
+ * the family-layer `MongoStorage` class instance (carrying the
25
+ * target-supplied `namespaces` map). Targets that need to add
26
+ * structural checks beyond the family default can override
27
+ * `parseMongoContractStructure`.
28
+ *
29
+ * Default `serializeContract` is identity over the contract — Mongo
30
+ * target classes carry JSON-clean fields by construction, so the value
31
+ * can be `JSON.stringify`'d directly. Targets that need on-the-way-out
32
+ * canonicalization override `serializeContract`.
33
+ */
34
+ export abstract class MongoContractSerializerBase<TContract>
35
+ implements ContractSerializer<TContract>
36
+ {
37
+ deserializeContract(json: unknown): TContract {
38
+ const validated = this.parseMongoContractStructure(json);
39
+ return this.constructTargetContract(validated);
40
+ }
41
+
42
+ serializeContract(contract: TContract): JsonObject {
43
+ // Mongo contract class fields are JSON-clean by construction; the
44
+ // cast asserts that. Targets that need to canonicalize on the way
45
+ // out override this method.
46
+ return contract as unknown as JsonObject;
47
+ }
48
+
49
+ /**
50
+ * Family-shared structural validation: parse against the Mongo
51
+ * contract arktype schema, then run framework-shared domain + Mongo
52
+ * family storage checks, then hydrate the validated tree into Mongo
53
+ * Contract IR class instances. Targets can override to add
54
+ * target-specific structural checks; most targets accept the family
55
+ * default.
56
+ *
57
+ * The returned `MongoContract` carries class instances under
58
+ * `storage.collections` (each value is a `MongoCollection`, with
59
+ * nested `MongoIndex` / `MongoValidator` / `MongoCollectionOptions`
60
+ * constructed by the `MongoCollection` constructor). The rest of the
61
+ * contract envelope (models, valueObjects, capabilities, …) remains
62
+ * in plain-JSON form; those IR layers are handled by sibling
63
+ * subsystems and don't sit behind this SPI.
64
+ */
65
+ protected parseMongoContractStructure(json: unknown): MongoContract {
66
+ const parsed = MongoContractSchema(json);
67
+ if (parsed instanceof arktypeType.errors) {
68
+ throw new Error(`Contract structural validation failed: ${parsed.summary}`);
69
+ }
70
+
71
+ // arktype's `infer`d type for `MongoContractSchema` is structurally
72
+ // equivalent to `MongoContract` (both describe the same on-disk JSON
73
+ // envelope) but not nominally so: the arktype DSL produces a type whose
74
+ // optional/readonly profile, narrowed string-literal positions, and
75
+ // utility-type wrappings (`Type.infer`, `Out`, …) differ from the
76
+ // hand-authored `MongoContract<S, M>` generic surface. The schema and
77
+ // the type are kept in lockstep by the round-trip fixtures under
78
+ // `test/validate.test.ts`. The hydration walk below additionally
79
+ // re-shapes `storage.collections` from plain data into IR-class
80
+ // instances, so the `MongoContract` returned here carries class
81
+ // identity under `storage.collections.*` (and transitively under
82
+ // `indexes` / `validator` / `options`).
83
+ const validatedShape = parsed as unknown as MongoContract;
84
+
85
+ const hydratedContract = this.hydrateMongoContract(validatedShape);
86
+
87
+ validateContractDomain(hydratedContract);
88
+ validateMongoStorage(hydratedContract);
89
+
90
+ return hydratedContract;
91
+ }
92
+
93
+ /**
94
+ * Walk a structurally-validated Mongo contract and convert
95
+ * `storage.collections` entries from plain data into
96
+ * `MongoCollection` IR-class instances. Idempotent: already-class
97
+ * instances pass through unchanged.
98
+ */
99
+ protected hydrateMongoContract(contract: MongoContract): MongoContract {
100
+ const rawCollections = contract.storage.collections;
101
+ const hydrated: Record<string, MongoCollection> = {};
102
+ for (const [name, raw] of Object.entries(rawCollections)) {
103
+ hydrated[name] =
104
+ raw instanceof MongoCollection ? raw : new MongoCollection(raw as MongoCollectionInput);
105
+ }
106
+ return {
107
+ ...contract,
108
+ storage: {
109
+ ...contract.storage,
110
+ collections: hydrated,
111
+ },
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Target-specific class construction from the validated structural
117
+ * data. The target wraps the contract envelope in the family-layer
118
+ * `MongoStorage` class instance, supplying the `namespaces` map
119
+ * (target concretions like `MongoTargetUnspecifiedDatabase`). The
120
+ * leaf collection / index shapes are already family-layer IR-class
121
+ * instances after the hydration walk above.
122
+ */
123
+ protected abstract constructTargetContract(validated: MongoContract): TContract;
124
+ }
@@ -0,0 +1,18 @@
1
+ import type { MongoContract } from '@prisma-next/mongo-contract';
2
+ import { MongoContractSerializerBase } from './mongo-contract-serializer-base';
3
+
4
+ /**
5
+ * Default Mongo family `ContractSerializer` concretion. Inherits the
6
+ * Mongo-shared deserialization pipeline (structural validation +
7
+ * collection-level hydration) and falls through `constructTargetContract`
8
+ * with the validated `MongoContract` shape. Family-level call sites
9
+ * (family-instance methods, family-layer tests that don't reach into
10
+ * a target descriptor) instantiate this directly; targets with their
11
+ * own storage concretion (`target-mongo`'s `MongoTargetContractSerializer`)
12
+ * override `constructTargetContract` to wrap the storage shape.
13
+ */
14
+ export class MongoContractSerializer extends MongoContractSerializerBase<MongoContract> {
15
+ protected constructTargetContract(validated: MongoContract): MongoContract {
16
+ return validated;
17
+ }
18
+ }
@@ -0,0 +1,87 @@
1
+ import type {
2
+ SchemaIssue,
3
+ SchemaVerifier,
4
+ SchemaVerifyOptions,
5
+ SchemaVerifyResult,
6
+ } from '@prisma-next/framework-components/control';
7
+ import type { Namespace } from '@prisma-next/framework-components/ir';
8
+ import type { MongoStorage } from '@prisma-next/mongo-contract';
9
+
10
+ /**
11
+ * Mongo family `SchemaVerifier` abstract base. Commits the Mongo family
12
+ * to namespace-keyed verification: the family-shared walk iterates
13
+ * `storage.namespaces` in sorted order and dispatches per-namespace
14
+ * through the protected `verifyNamespace` hook, then aggregates
15
+ * target-extension issues from `verifyTargetExtensions`.
16
+ *
17
+ * Per-element diff work (collection / index / validator comparisons)
18
+ * lives on the target inside `verifyNamespace`. The family's structural
19
+ * commitment is "verification is namespaced"; the target's commitment is
20
+ * "verification of a given namespace's collections is the existing
21
+ * diff/canonicalize pipeline". The split keeps target-mongo's
22
+ * introspection-side helpers (`contractToMongoSchemaIR`,
23
+ * `canonicalizeSchemasForVerification`, `diffMongoSchemas`) in the target
24
+ * layer where they belong, while the family base owns the iteration
25
+ * scaffolding that makes namespaces a first-class verifier concept.
26
+ *
27
+ * Target-specific issue kinds (Atlas-only, future RLS-equivalents)
28
+ * surface through `verifyTargetExtensions`; that hook returns the empty
29
+ * list when no extensions exist over the Mongo family alphabet.
30
+ */
31
+ export abstract class MongoSchemaVerifierBase<
32
+ TContract extends { readonly storage: MongoStorage },
33
+ TSchema,
34
+ > implements SchemaVerifier<TContract, TSchema>
35
+ {
36
+ verifySchema(options: SchemaVerifyOptions<TContract, TSchema>): SchemaVerifyResult {
37
+ const issues: SchemaIssue[] = [];
38
+ issues.push(...this.verifyCommonMongoSchema(options));
39
+ issues.push(...this.verifyTargetExtensions(options));
40
+ return { ok: issues.length === 0, issues };
41
+ }
42
+
43
+ protected verifyCommonMongoSchema(
44
+ options: SchemaVerifyOptions<TContract, TSchema>,
45
+ ): readonly SchemaIssue[] {
46
+ const issues: SchemaIssue[] = [];
47
+ const { namespaces } = options.contract.storage;
48
+ const namespaceIds = Object.keys(namespaces).sort();
49
+ for (const namespaceId of namespaceIds) {
50
+ const namespace = namespaces[namespaceId];
51
+ if (!namespace) continue;
52
+ issues.push(
53
+ ...this.verifyNamespace({
54
+ contract: options.contract,
55
+ schema: options.schema,
56
+ namespaceId,
57
+ namespace,
58
+ }),
59
+ );
60
+ }
61
+ return issues;
62
+ }
63
+
64
+ /**
65
+ * Per-namespace verification hook. Receives the namespace metadata plus
66
+ * the full contract + schema pair; the target's implementation owns the
67
+ * per-collection diff using its existing introspection-side helpers.
68
+ * Slice the schema by namespace at the call site (or compute the full
69
+ * diff once and dispatch per namespace) — the family base does not
70
+ * prescribe the per-namespace shape.
71
+ */
72
+ protected abstract verifyNamespace(options: {
73
+ readonly contract: TContract;
74
+ readonly schema: TSchema;
75
+ readonly namespaceId: string;
76
+ readonly namespace: Namespace;
77
+ }): readonly SchemaIssue[];
78
+
79
+ /**
80
+ * Target-specific extensions — Atlas-only kinds, target-only
81
+ * namespace-mismatch issues that don't fit the family-shared walk.
82
+ * Returns the empty list when the target ships no extensions.
83
+ */
84
+ protected abstract verifyTargetExtensions(
85
+ options: SchemaVerifyOptions<TContract, TSchema>,
86
+ ): readonly SchemaIssue[];
87
+ }
@@ -0,0 +1,131 @@
1
+ import type {
2
+ MigrationPlanOperation,
3
+ OperationPreview,
4
+ } from '@prisma-next/framework-components/control';
5
+ import type {
6
+ CollModCommand,
7
+ CreateCollectionCommand,
8
+ CreateIndexCommand,
9
+ DropCollectionCommand,
10
+ DropIndexCommand,
11
+ MongoDdlCommandVisitor,
12
+ MongoIndexKey,
13
+ } from '@prisma-next/mongo-query-ast/control';
14
+
15
+ function formatKeySpec(keys: ReadonlyArray<MongoIndexKey>): string {
16
+ const entries = keys.map((k) => `${JSON.stringify(k.field)}: ${JSON.stringify(k.direction)}`);
17
+ return `{ ${entries.join(', ')} }`;
18
+ }
19
+
20
+ function formatOptions(cmd: CreateIndexCommand): string | undefined {
21
+ const parts: string[] = [];
22
+ if (cmd.unique) parts.push('unique: true');
23
+ if (cmd.sparse) parts.push('sparse: true');
24
+ if (cmd.expireAfterSeconds !== undefined)
25
+ parts.push(`expireAfterSeconds: ${cmd.expireAfterSeconds}`);
26
+ if (cmd.name) parts.push(`name: ${JSON.stringify(cmd.name)}`);
27
+ if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
28
+ if (cmd.weights) parts.push(`weights: ${JSON.stringify(cmd.weights)}`);
29
+ if (cmd.default_language) parts.push(`default_language: ${JSON.stringify(cmd.default_language)}`);
30
+ if (cmd.language_override)
31
+ parts.push(`language_override: ${JSON.stringify(cmd.language_override)}`);
32
+ if (cmd.wildcardProjection)
33
+ parts.push(`wildcardProjection: ${JSON.stringify(cmd.wildcardProjection)}`);
34
+ if (cmd.partialFilterExpression)
35
+ parts.push(`partialFilterExpression: ${JSON.stringify(cmd.partialFilterExpression)}`);
36
+ if (parts.length === 0) return undefined;
37
+ return `{ ${parts.join(', ')} }`;
38
+ }
39
+
40
+ function formatCreateCollectionOptions(cmd: CreateCollectionCommand): string | undefined {
41
+ const parts: string[] = [];
42
+ if (cmd.capped) parts.push('capped: true');
43
+ if (cmd.size !== undefined) parts.push(`size: ${cmd.size}`);
44
+ if (cmd.max !== undefined) parts.push(`max: ${cmd.max}`);
45
+ if (cmd.timeseries) parts.push(`timeseries: ${JSON.stringify(cmd.timeseries)}`);
46
+ if (cmd.collation) parts.push(`collation: ${JSON.stringify(cmd.collation)}`);
47
+ if (cmd.clusteredIndex) parts.push(`clusteredIndex: ${JSON.stringify(cmd.clusteredIndex)}`);
48
+ if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
49
+ if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
50
+ if (cmd.validationAction) parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
51
+ if (cmd.changeStreamPreAndPostImages)
52
+ parts.push(`changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`);
53
+ if (parts.length === 0) return undefined;
54
+ return `{ ${parts.join(', ')} }`;
55
+ }
56
+
57
+ class MongoDdlCommandFormatter implements MongoDdlCommandVisitor<string> {
58
+ createIndex(cmd: CreateIndexCommand): string {
59
+ const keySpec = formatKeySpec(cmd.keys);
60
+ const opts = formatOptions(cmd);
61
+ return opts
62
+ ? `db.${cmd.collection}.createIndex(${keySpec}, ${opts})`
63
+ : `db.${cmd.collection}.createIndex(${keySpec})`;
64
+ }
65
+
66
+ dropIndex(cmd: DropIndexCommand): string {
67
+ return `db.${cmd.collection}.dropIndex(${JSON.stringify(cmd.name)})`;
68
+ }
69
+
70
+ createCollection(cmd: CreateCollectionCommand): string {
71
+ const opts = formatCreateCollectionOptions(cmd);
72
+ return opts
73
+ ? `db.createCollection(${JSON.stringify(cmd.collection)}, ${opts})`
74
+ : `db.createCollection(${JSON.stringify(cmd.collection)})`;
75
+ }
76
+
77
+ dropCollection(cmd: DropCollectionCommand): string {
78
+ return `db.${cmd.collection}.drop()`;
79
+ }
80
+
81
+ collMod(cmd: CollModCommand): string {
82
+ const parts: string[] = [`collMod: ${JSON.stringify(cmd.collection)}`];
83
+ if (cmd.validator) parts.push(`validator: ${JSON.stringify(cmd.validator)}`);
84
+ if (cmd.validationLevel) parts.push(`validationLevel: ${JSON.stringify(cmd.validationLevel)}`);
85
+ if (cmd.validationAction)
86
+ parts.push(`validationAction: ${JSON.stringify(cmd.validationAction)}`);
87
+ if (cmd.changeStreamPreAndPostImages)
88
+ parts.push(
89
+ `changeStreamPreAndPostImages: ${JSON.stringify(cmd.changeStreamPreAndPostImages)}`,
90
+ );
91
+ return `db.runCommand({ ${parts.join(', ')} })`;
92
+ }
93
+ }
94
+
95
+ const formatter = new MongoDdlCommandFormatter();
96
+
97
+ interface MongoExecuteStep {
98
+ readonly command: { readonly accept: <R>(visitor: MongoDdlCommandVisitor<R>) => R };
99
+ }
100
+
101
+ export function formatMongoOperations(operations: readonly MigrationPlanOperation[]): string[] {
102
+ const statements: string[] = [];
103
+ for (const operation of operations) {
104
+ const candidate = operation as unknown as Record<string, unknown>;
105
+ if (!('execute' in candidate) || !Array.isArray(candidate['execute'])) {
106
+ continue;
107
+ }
108
+ for (const step of candidate['execute'] as MongoExecuteStep[]) {
109
+ if (step.command && typeof step.command.accept === 'function') {
110
+ statements.push(step.command.accept(formatter));
111
+ }
112
+ }
113
+ }
114
+ return statements;
115
+ }
116
+
117
+ /**
118
+ * Wraps `formatMongoOperations` into the family-agnostic
119
+ * `OperationPreview` shape. Each statement carries
120
+ * `language: 'mongodb-shell'`. Mirrors `sqlOperationsToPreview`.
121
+ */
122
+ export function mongoOperationsToPreview(
123
+ operations: readonly MigrationPlanOperation[],
124
+ ): OperationPreview {
125
+ return {
126
+ statements: formatMongoOperations(operations).map((text) => ({
127
+ text,
128
+ language: 'mongodb-shell',
129
+ })),
130
+ };
131
+ }