@prisma-next/sql-contract-ts 0.3.0-dev.134 → 0.3.0-dev.135

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.
@@ -0,0 +1,245 @@
1
+ import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
2
+ import {
3
+ type ModelAttributesSpec,
4
+ normalizeRelationFieldNames,
5
+ type RelationBuilder,
6
+ type ScalarFieldBuilder,
7
+ type SqlStageSpec,
8
+ type StagedModelBuilder,
9
+ type RelationState as StagedRelationState,
10
+ } from './staged-contract-dsl';
11
+
12
+ type RuntimeStagedModel = StagedModelBuilder<
13
+ string | undefined,
14
+ Record<string, ScalarFieldBuilder>,
15
+ Record<string, RelationBuilder<StagedRelationState>>,
16
+ ModelAttributesSpec | undefined,
17
+ SqlStageSpec | undefined
18
+ >;
19
+
20
+ type RuntimeModelSpec = {
21
+ readonly modelName: string;
22
+ readonly tableName: string;
23
+ readonly relations: Record<string, RelationBuilder<StagedRelationState>>;
24
+ readonly sqlSpec: SqlStageSpec | undefined;
25
+ };
26
+
27
+ type RuntimeStagedCollection = {
28
+ readonly storageTypes: Record<string, StorageTypeInstance>;
29
+ readonly models: Record<string, RuntimeStagedModel>;
30
+ readonly modelSpecs: ReadonlyMap<string, RuntimeModelSpec>;
31
+ };
32
+
33
+ function hasNamedModelToken(
34
+ models: Record<string, RuntimeStagedModel>,
35
+ modelName: string,
36
+ ): boolean {
37
+ return models[modelName]?.stageOne.modelName === modelName;
38
+ }
39
+
40
+ function formatFieldSelection(fieldNames: readonly string[]): string {
41
+ if (fieldNames.length === 1) {
42
+ return `'${fieldNames[0]}'`;
43
+ }
44
+
45
+ return `[${fieldNames.map((fieldName) => `'${fieldName}'`).join(', ')}]`;
46
+ }
47
+
48
+ function formatTokenFieldSelection(modelName: string, fieldNames: readonly string[]): string {
49
+ if (fieldNames.length === 1) {
50
+ return `${modelName}.refs.${fieldNames[0]}`;
51
+ }
52
+
53
+ return `[${fieldNames.map((fieldName) => `${modelName}.refs.${fieldName}`).join(', ')}]`;
54
+ }
55
+
56
+ function formatConstraintsRefCall(modelName: string, fieldNames: readonly string[]): string {
57
+ if (fieldNames.length === 1) {
58
+ return `constraints.ref('${modelName}', '${fieldNames[0]}')`;
59
+ }
60
+
61
+ return `[${fieldNames
62
+ .map((fieldName) => `constraints.ref('${modelName}', '${fieldName}')`)
63
+ .join(', ')}]`;
64
+ }
65
+
66
+ function formatRelationModelDisplay(
67
+ relationModel:
68
+ | StagedRelationState['toModel']
69
+ | Extract<StagedRelationState, { kind: 'manyToMany' }>['through'],
70
+ ): string {
71
+ if (relationModel.kind === 'lazyRelationModelName') {
72
+ return `() => ${relationModel.resolve()}`;
73
+ }
74
+
75
+ return relationModel.source === 'string'
76
+ ? `'${relationModel.modelName}'`
77
+ : relationModel.modelName;
78
+ }
79
+
80
+ function formatRelationCall(relation: StagedRelationState, targetModelDisplay: string): string {
81
+ if (relation.kind === 'belongsTo') {
82
+ const from = formatFieldSelection(normalizeRelationFieldNames(relation.from));
83
+ const to = formatFieldSelection(normalizeRelationFieldNames(relation.to));
84
+ return `rel.belongsTo(${targetModelDisplay}, { from: ${from}, to: ${to} })`;
85
+ }
86
+
87
+ if (relation.kind === 'hasMany' || relation.kind === 'hasOne') {
88
+ const by = formatFieldSelection(normalizeRelationFieldNames(relation.by));
89
+ return `rel.${relation.kind}(${targetModelDisplay}, { by: ${by} })`;
90
+ }
91
+
92
+ const throughDisplay = formatRelationModelDisplay(relation.through);
93
+ const from = formatFieldSelection(normalizeRelationFieldNames(relation.from));
94
+ const to = formatFieldSelection(normalizeRelationFieldNames(relation.to));
95
+ return `rel.manyToMany(${targetModelDisplay}, { through: ${throughDisplay}, from: ${from}, to: ${to} })`;
96
+ }
97
+
98
+ function formatManyToManyCallWithThrough(
99
+ relation: Extract<StagedRelationState, { kind: 'manyToMany' }>,
100
+ throughDisplay: string,
101
+ ): string {
102
+ const targetDisplay = formatRelationModelDisplay(relation.toModel);
103
+ const from = formatFieldSelection(normalizeRelationFieldNames(relation.from));
104
+ const to = formatFieldSelection(normalizeRelationFieldNames(relation.to));
105
+ return `rel.manyToMany(${targetDisplay}, { through: ${throughDisplay}, from: ${from}, to: ${to} })`;
106
+ }
107
+
108
+ const WARNING_BATCH_THRESHOLD = 5;
109
+
110
+ function flushWarnings(warnings: readonly string[]): void {
111
+ if (warnings.length === 0) {
112
+ return;
113
+ }
114
+
115
+ if (warnings.length <= WARNING_BATCH_THRESHOLD) {
116
+ for (const message of warnings) {
117
+ process.emitWarning(message, { code: 'PN_CONTRACT_TYPED_FALLBACK_AVAILABLE' });
118
+ }
119
+ return;
120
+ }
121
+
122
+ process.emitWarning(
123
+ `${warnings.length} staged contract references use string fallbacks where typed alternatives are available. ` +
124
+ 'Use named model tokens and typed storage type refs for autocomplete and type safety.\n' +
125
+ warnings.map((w) => ` - ${w}`).join('\n'),
126
+ { code: 'PN_CONTRACT_TYPED_FALLBACK_AVAILABLE' },
127
+ );
128
+ }
129
+
130
+ function formatFallbackWarning(location: string, current: string, suggested: string): string {
131
+ return (
132
+ `Staged contract ${location} uses ${current}. ` +
133
+ `Use ${suggested} when the named model token is available in the same contract to keep typed relation targets and model refs.`
134
+ );
135
+ }
136
+
137
+ export function emitTypedNamedTypeFallbackWarnings(
138
+ models: Record<string, RuntimeStagedModel>,
139
+ storageTypes: Record<string, StorageTypeInstance>,
140
+ ): void {
141
+ const warnings: string[] = [];
142
+ const warnedFields = new Set<string>();
143
+
144
+ for (const [modelName, modelDefinition] of Object.entries(models)) {
145
+ for (const [fieldName, fieldBuilder] of Object.entries(modelDefinition.stageOne.fields)) {
146
+ const fieldState = fieldBuilder.build();
147
+ if (typeof fieldState.typeRef !== 'string' || !(fieldState.typeRef in storageTypes)) {
148
+ continue;
149
+ }
150
+
151
+ const warningKey = `${modelName}.${fieldName}`;
152
+ if (warnedFields.has(warningKey)) {
153
+ continue;
154
+ }
155
+ warnedFields.add(warningKey);
156
+
157
+ warnings.push(
158
+ `Staged contract field "${modelName}.${fieldName}" uses field.namedType('${fieldState.typeRef}'). ` +
159
+ `Use field.namedType(types.${fieldState.typeRef}) when the storage type is declared in the same contract to keep autocomplete and typed local refs.`,
160
+ );
161
+ }
162
+ }
163
+
164
+ flushWarnings(warnings);
165
+ }
166
+
167
+ export function emitTypedCrossModelFallbackWarnings(collection: RuntimeStagedCollection): void {
168
+ const warnings: string[] = [];
169
+ const warnedKeys = new Set<string>();
170
+
171
+ for (const spec of collection.modelSpecs.values()) {
172
+ for (const [relationName, relationBuilder] of Object.entries(spec.relations)) {
173
+ const relation = relationBuilder.build();
174
+
175
+ if (
176
+ relation.toModel.kind === 'relationModelName' &&
177
+ relation.toModel.source === 'string' &&
178
+ hasNamedModelToken(collection.models, relation.toModel.modelName)
179
+ ) {
180
+ const warningKey = `${spec.modelName}.${relationName}.toModel`;
181
+ if (!warnedKeys.has(warningKey)) {
182
+ warnedKeys.add(warningKey);
183
+
184
+ const current = formatRelationCall(relation, `'${relation.toModel.modelName}'`);
185
+ const suggested = formatRelationCall(relation, relation.toModel.modelName);
186
+ warnings.push(
187
+ formatFallbackWarning(
188
+ `relation "${spec.modelName}.${relationName}"`,
189
+ current,
190
+ suggested,
191
+ ),
192
+ );
193
+ }
194
+ }
195
+
196
+ if (
197
+ relation.kind === 'manyToMany' &&
198
+ relation.through.kind === 'relationModelName' &&
199
+ relation.through.source === 'string' &&
200
+ hasNamedModelToken(collection.models, relation.through.modelName)
201
+ ) {
202
+ const warningKey = `${spec.modelName}.${relationName}.through`;
203
+ if (!warnedKeys.has(warningKey)) {
204
+ warnedKeys.add(warningKey);
205
+
206
+ const current = formatManyToManyCallWithThrough(
207
+ relation,
208
+ `'${relation.through.modelName}'`,
209
+ );
210
+ const suggested = formatManyToManyCallWithThrough(relation, relation.through.modelName);
211
+ warnings.push(
212
+ formatFallbackWarning(
213
+ `relation "${spec.modelName}.${relationName}"`,
214
+ current,
215
+ suggested,
216
+ ),
217
+ );
218
+ }
219
+ }
220
+ }
221
+
222
+ for (const [foreignKeyIndex, foreignKey] of (spec.sqlSpec?.foreignKeys ?? []).entries()) {
223
+ if (
224
+ foreignKey.targetSource !== 'string' ||
225
+ !hasNamedModelToken(collection.models, foreignKey.targetModel)
226
+ ) {
227
+ continue;
228
+ }
229
+
230
+ const warningKey = `${spec.modelName}.sql.foreignKeys.${foreignKeyIndex}`;
231
+ if (warnedKeys.has(warningKey)) {
232
+ continue;
233
+ }
234
+ warnedKeys.add(warningKey);
235
+
236
+ const current = formatConstraintsRefCall(foreignKey.targetModel, foreignKey.targetFields);
237
+ const suggested = formatTokenFieldSelection(foreignKey.targetModel, foreignKey.targetFields);
238
+ warnings.push(
239
+ formatFallbackWarning(`model "${spec.modelName}"`, `${current} in .sql(...)`, suggested),
240
+ );
241
+ }
242
+ }
243
+
244
+ flushWarnings(warnings);
245
+ }