@prisma-next/mongo-contract 0.11.0 → 0.12.0

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.
@@ -3,73 +3,15 @@ import {
3
3
  freezeNode,
4
4
  IRNodeBase,
5
5
  type Namespace,
6
- NamespaceBase,
7
6
  type Storage,
8
- UNBOUND_NAMESPACE_ID,
9
7
  } from '@prisma-next/framework-components/ir';
10
- import { MongoCollection, type MongoCollectionInput } from './mongo-collection';
11
- import { MongoUnboundNamespace } from './mongo-unbound-namespace';
8
+ import type { MongoCollection, MongoCollectionInput } from './mongo-collection';
12
9
 
13
10
  export interface MongoNamespaceCollectionsInput {
14
11
  readonly id: string;
15
12
  readonly collections?: Record<string, MongoCollection | MongoCollectionInput>;
16
13
  }
17
14
 
18
- export interface MongoStorageInput<THash extends string = string> {
19
- readonly storageHash: StorageHashBase<THash>;
20
- readonly namespaces?: Readonly<Record<string, Namespace | MongoNamespaceCollectionsInput>>;
21
- }
22
-
23
- const DEFAULT_NAMESPACES: Readonly<Record<string, Namespace>> = Object.freeze({
24
- [UNBOUND_NAMESPACE_ID]: MongoUnboundNamespace.instance,
25
- });
26
-
27
- class MongoNamespacePayload extends NamespaceBase {
28
- declare readonly kind: string;
29
-
30
- readonly id: string;
31
- readonly collections: Readonly<Record<string, MongoCollection>>;
32
-
33
- constructor(input: MongoNamespaceCollectionsInput) {
34
- super();
35
- this.id = input.id;
36
- this.collections = Object.freeze(
37
- Object.fromEntries(
38
- Object.entries(input.collections ?? {}).map(([name, c]) => [
39
- name,
40
- c instanceof MongoCollection ? c : new MongoCollection(c),
41
- ]),
42
- ),
43
- );
44
- Object.defineProperty(this, 'kind', {
45
- value: 'mongo-namespace',
46
- writable: false,
47
- enumerable: false,
48
- configurable: true,
49
- });
50
- freezeNode(this);
51
- }
52
- }
53
-
54
- function normaliseNamespaceEntry(
55
- nsKey: string,
56
- ns: Namespace | MongoNamespaceCollectionsInput,
57
- ): Namespace {
58
- if (ns instanceof NamespaceBase) {
59
- return ns;
60
- }
61
- // The framework `Namespace` interface only promises `id`; the remaining
62
- // arm of this union — plain-object inputs accepted by `MongoStorageInput`
63
- // — is `MongoNamespaceCollectionsInput`. The `instanceof` guard above
64
- // discriminates the two; TypeScript can't narrow further without a hint.
65
- const input = ns as MongoNamespaceCollectionsInput;
66
- const collectionCount = Object.keys(input.collections ?? {}).length;
67
- if (nsKey === UNBOUND_NAMESPACE_ID && collectionCount === 0) {
68
- return MongoUnboundNamespace.instance;
69
- }
70
- return new MongoNamespacePayload(input);
71
- }
72
-
73
15
  // Mongo concretions always store `MongoCollection` instances in
74
16
  // `collections` (Mongo idiom — distinct from the SQL family's `tables`).
75
17
  // Narrowing the namespace map here lets target/family-level consumers
@@ -80,6 +22,11 @@ export type MongoNamespace = Namespace & {
80
22
  readonly collections: Readonly<Record<string, MongoCollection>>;
81
23
  };
82
24
 
25
+ export interface MongoStorageInput<THash extends string = string> {
26
+ readonly storageHash: StorageHashBase<THash>;
27
+ readonly namespaces: Readonly<Record<string, MongoNamespace>>;
28
+ }
29
+
83
30
  export class MongoStorage<THash extends string = string> extends IRNodeBase implements Storage {
84
31
  declare readonly kind: 'mongo-storage';
85
32
  readonly storageHash: StorageHashBase<THash>;
@@ -94,14 +41,7 @@ export class MongoStorage<THash extends string = string> extends IRNodeBase impl
94
41
  configurable: true,
95
42
  });
96
43
  this.storageHash = input.storageHash;
97
- this.namespaces = Object.freeze(
98
- Object.fromEntries(
99
- Object.entries(input.namespaces ?? DEFAULT_NAMESPACES).map(([nsKey, ns]) => [
100
- nsKey,
101
- normaliseNamespaceEntry(nsKey, ns) as MongoNamespace,
102
- ]),
103
- ),
104
- );
44
+ this.namespaces = Object.freeze(input.namespaces);
105
45
  freezeNode(this);
106
46
  }
107
47
  }
@@ -1,4 +1,8 @@
1
- import type { MongoContract } from './contract-types';
1
+ import type { MongoContract, MongoModelDefinition } from './contract-types';
2
+
3
+ function formatCrossRef(crossRef: { readonly namespace: string; readonly model: string }): string {
4
+ return `${crossRef.namespace}.${crossRef.model}`;
5
+ }
2
6
 
3
7
  function storageDeclaresCollection(
4
8
  storage: MongoContract['storage'],
@@ -15,62 +19,65 @@ function storageDeclaresCollection(
15
19
  export function validateMongoStorage(contract: MongoContract): void {
16
20
  const errors: string[] = [];
17
21
 
18
- for (const [modelName, model] of Object.entries(contract.models)) {
19
- if (
20
- model.storage.collection &&
21
- !storageDeclaresCollection(contract.storage, model.storage.collection)
22
- ) {
23
- errors.push(
24
- `Model "${modelName}" references collection "${model.storage.collection}" which is not declared under any namespace's collections map`,
25
- );
26
- }
22
+ for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) {
23
+ const models = namespace.models as Record<string, MongoModelDefinition>;
24
+ for (const [modelName, model] of Object.entries(models)) {
25
+ const qualifiedName = `${namespaceId}:${modelName}`;
26
+ if (
27
+ model.storage.collection &&
28
+ !storageDeclaresCollection(contract.storage, model.storage.collection)
29
+ ) {
30
+ errors.push(
31
+ `Model "${qualifiedName}" references collection "${model.storage.collection}" which is not declared under any namespace's collections map`,
32
+ );
33
+ }
27
34
 
28
- // Mongo does not support multi-table inheritance (ADR 2): all variants of a base
29
- // must share the same collection (single-table inheritance only).
30
- if (model.base) {
31
- const baseModel = contract.models[model.base];
32
- if (baseModel) {
33
- const variantCollection = model.storage.collection;
34
- const baseCollection = baseModel.storage.collection;
35
- if (variantCollection !== baseCollection) {
36
- errors.push(
37
- `Mongo does not support multi-table inheritance; variant "${modelName}" must share its base's collection ("${baseCollection ?? '(none)'}"), but has "${variantCollection ?? '(none)'}"`,
38
- );
35
+ if (model.base) {
36
+ const baseModel = models[model.base.model];
37
+ if (baseModel) {
38
+ const variantCollection = model.storage.collection;
39
+ const baseCollection = baseModel.storage.collection;
40
+ if (variantCollection !== baseCollection) {
41
+ errors.push(
42
+ `Mongo does not support multi-table inheritance; variant "${qualifiedName}" must share its base's collection ("${baseCollection ?? '(none)'}"), but has "${variantCollection ?? '(none)'}"`,
43
+ );
44
+ }
39
45
  }
40
46
  }
41
- }
42
47
 
43
- for (const [relName, relation] of Object.entries(model.relations ?? {})) {
44
- const targetModel = contract.models[relation.to];
48
+ for (const [relName, relation] of Object.entries(model.relations ?? {})) {
49
+ const targetModel = models[relation.to.model];
50
+ const targetLabel = formatCrossRef(relation.to);
45
51
 
46
- if (targetModel?.owner) {
47
- if (targetModel.owner !== modelName) {
48
- errors.push(
49
- `Embed relation "${relName}" targets "${relation.to}" which is owned by "${targetModel.owner}", not "${modelName}"`,
50
- );
51
- }
52
- if (targetModel.storage.collection) {
53
- errors.push(
54
- `Embed relation "${relName}" targets "${relation.to}" which must not have a collection`,
55
- );
56
- }
57
- } else if ('on' in relation && relation.on) {
58
- for (const localField of relation.on.localFields) {
59
- if (!(localField in model.fields)) {
52
+ if (targetModel?.owner) {
53
+ if (targetModel.owner !== modelName) {
60
54
  errors.push(
61
- `Reference relation "${relName}": localField "${localField}" is not a field on model "${modelName}"`,
55
+ `Embed relation "${relName}" targets "${targetLabel}" which is owned by "${targetModel.owner}", not "${qualifiedName}"`,
62
56
  );
63
57
  }
64
- }
65
-
66
- if (targetModel) {
67
- for (const targetField of relation.on.targetFields) {
68
- if (!(targetField in targetModel.fields)) {
58
+ if (targetModel.storage.collection) {
59
+ errors.push(
60
+ `Embed relation "${relName}" targets "${targetLabel}" which must not have a collection`,
61
+ );
62
+ }
63
+ } else if ('on' in relation && relation.on) {
64
+ for (const localField of relation.on.localFields) {
65
+ if (!(localField in model.fields)) {
69
66
  errors.push(
70
- `Reference relation "${relName}": targetField "${targetField}" is not a field on model "${relation.to}"`,
67
+ `Reference relation "${relName}": localField "${localField}" is not a field on model "${qualifiedName}"`,
71
68
  );
72
69
  }
73
70
  }
71
+
72
+ if (targetModel) {
73
+ for (const targetField of relation.on.targetFields) {
74
+ if (!(targetField in targetModel.fields)) {
75
+ errors.push(
76
+ `Reference relation "${relName}": targetField "${targetField}" is not a field on model "${targetLabel}"`,
77
+ );
78
+ }
79
+ }
80
+ }
74
81
  }
75
82
  }
76
83
  }