Skip to content

Commit fa181e3

Browse files
TML-2837: eliminate the defineContract cast pattern in SQL extension wrappers via a shared bound builder (#731)
Introduces a shared family/target-bound `buildBoundContract` in the SQL contract-builder; the sqlite and postgres extension `defineContract` wrappers delegate to it cast-free (4→0 casts each). The one residual helpers-composition cast stays in its single existing home. Also includes the factory-clobber hardening (TML-2839): `buildBoundContract` spreads only the factory's declared outputs (`types`/`models`) via `ifDefined`, so factory output cannot override the pre-bound `family`/`target`/`extensionPacks`. Refs: TML-2837, TML-2839 Signed-off-by: Will Madden <madden@prisma.io>
1 parent f061a47 commit fa181e3

5 files changed

Lines changed: 161 additions & 69 deletions

File tree

packages/2-sql/2-authoring/contract-ts/src/contract-builder.ts

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
StorageTypeInstance,
1414
} from '@prisma-next/sql-contract/types';
1515
import { blindCast } from '@prisma-next/utils/casts';
16+
import { ifDefined } from '@prisma-next/utils/defined';
1617
import { buildSqlContractFromDefinition } from './build-contract';
1718
import {
1819
type ComposedAuthoringHelpers,
@@ -292,6 +293,130 @@ function buildContractFromDsl<Definition extends ContractInput>(
292293
>(buildSqlContractFromDefinition(buildContractDefinition(definition), definition.codecLookup));
293294
}
294295

296+
// Input for buildBoundContract — all fields from ContractInput except family/target
297+
// (those are injected by the builder, pre-bound at the call site).
298+
type BoundDefinitionInput<
299+
Types extends Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = Record<
300+
never,
301+
never
302+
>,
303+
Models extends Record<string, ModelLike> = Record<never, never>,
304+
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined = undefined,
305+
Naming extends ContractInput['naming'] | undefined = undefined,
306+
StorageHash extends string | undefined = undefined,
307+
ForeignKeyDefaults extends ForeignKeyDefaultsState | undefined = undefined,
308+
Namespaces extends readonly string[] | undefined = undefined,
309+
> = {
310+
readonly extensionPacks?: ExtensionPacks;
311+
readonly naming?: Naming;
312+
readonly storageHash?: StorageHash;
313+
readonly foreignKeyDefaults?: ForeignKeyDefaults;
314+
readonly defaultControlPolicy?: ControlPolicy;
315+
readonly namespaces?: Namespaces;
316+
readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
317+
readonly types?: Types;
318+
readonly models?: Models;
319+
readonly codecLookup?: CodecLookup;
320+
};
321+
322+
// Merges a bound input with the pre-bound family/target to produce a full ContractDefinition.
323+
type WithFamilyTarget<
324+
Input,
325+
F extends FamilyPackRef<string>,
326+
T extends TargetPackRef<'sql', string>,
327+
> = Input & { readonly family: F; readonly target: T };
328+
329+
/**
330+
* Shared builder that assembles a SqlContract with pre-bound family and target.
331+
* Extension wrappers keep their own public overloads and delegate their impl body here;
332+
* this is a plain overloaded function (not a factory returning an overloaded function)
333+
* so no overloaded-function-return cast is needed.
334+
*
335+
* Overload 1: definition form (no factory).
336+
*/
337+
export function buildBoundContract<
338+
const F extends FamilyPackRef<string>,
339+
const T extends TargetPackRef<'sql', string>,
340+
const Definition extends BoundDefinitionInput<
341+
Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
342+
Record<string, ModelLike>,
343+
Record<string, ExtensionPackRef<'sql', string>> | undefined,
344+
ContractInput['naming'] | undefined,
345+
string | undefined,
346+
ForeignKeyDefaultsState | undefined,
347+
readonly string[] | undefined
348+
>,
349+
>(
350+
family: F,
351+
target: T,
352+
definition: Definition,
353+
factory?: undefined,
354+
): SqlContractResult<WithFamilyTarget<Definition, F, T>>;
355+
/**
356+
* Overload 2: factory form.
357+
*/
358+
export function buildBoundContract<
359+
const F extends FamilyPackRef<string>,
360+
const T extends TargetPackRef<'sql', string>,
361+
const Definition extends BoundDefinitionInput<
362+
Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
363+
Record<string, ModelLike>,
364+
Record<string, ExtensionPackRef<'sql', string>> | undefined,
365+
ContractInput['naming'] | undefined,
366+
string | undefined,
367+
ForeignKeyDefaultsState | undefined,
368+
readonly string[] | undefined
369+
>,
370+
const Built extends {
371+
readonly types?: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
372+
readonly models?: Record<string, ModelLike>;
373+
},
374+
>(
375+
family: F,
376+
target: T,
377+
definition: Definition,
378+
factory: (
379+
helpers: ComposedAuthoringHelpers<F, T, NonNullable<Definition['extensionPacks']>>,
380+
) => Built,
381+
): SqlContractResult<WithFamilyTarget<Definition & Built, F, T>>;
382+
/** Implementation. */
383+
export function buildBoundContract(
384+
family: FamilyPackRef<string>,
385+
target: TargetPackRef<'sql', string>,
386+
definition: Omit<ContractInput, 'family' | 'target'>,
387+
factory?:
388+
| ((
389+
helpers: ComposedAuthoringHelpers<
390+
FamilyPackRef<string>,
391+
TargetPackRef<'sql', string>,
392+
Record<string, ExtensionPackRef<'sql', string>> | undefined
393+
>,
394+
) => {
395+
readonly types?: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>;
396+
readonly models?: Record<string, ModelLike>;
397+
})
398+
| undefined,
399+
) {
400+
const full = { ...definition, family, target };
401+
402+
if (factory !== undefined) {
403+
const built = factory(
404+
createComposedAuthoringHelpers({
405+
family,
406+
target,
407+
extensionPacks: definition.extensionPacks,
408+
}),
409+
);
410+
return buildContractFromDsl({
411+
...full,
412+
...ifDefined('types', built.types),
413+
...ifDefined('models', built.models),
414+
});
415+
}
416+
417+
return buildContractFromDsl(full);
418+
}
419+
295420
export function defineContract<
296421
const Family extends FamilyPackRef<string>,
297422
const Target extends TargetPackRef<'sql', string>,
@@ -387,22 +512,10 @@ export function defineContract(
387512
);
388513
}
389514

390-
if (!factory) {
391-
return buildContractFromDsl(definition);
515+
if (factory !== undefined) {
516+
return buildBoundContract(definition.family, definition.target, definition, factory);
392517
}
393-
394-
const builtDefinition = {
395-
...definition,
396-
...factory(
397-
createComposedAuthoringHelpers({
398-
family: definition.family,
399-
target: definition.target,
400-
extensionPacks: definition.extensionPacks,
401-
}),
402-
),
403-
};
404-
405-
return buildContractFromDsl(builtDefinition);
518+
return buildBoundContract(definition.family, definition.target, definition);
406519
}
407520

408521
export type {

packages/2-sql/2-authoring/contract-ts/src/exports/contract-builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {
66
ScalarFieldBuilder,
77
} from '../contract-builder';
88
export {
9+
buildBoundContract,
910
buildSqlContractFromDefinition,
1011
defineContract,
1112
field,

packages/3-extensions/postgres/src/contract/define-contract.ts

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
ContractInput,
1010
ModelLike,
1111
} from '@prisma-next/sql-contract-ts/contract-builder';
12-
import { defineContract as baseDefineContract } from '@prisma-next/sql-contract-ts/contract-builder';
12+
import { buildBoundContract } from '@prisma-next/sql-contract-ts/contract-builder';
1313
import postgresPack from '@prisma-next/target-postgres/pack';
1414
import { postgresCreateNamespace } from '@prisma-next/target-postgres/types';
1515

@@ -23,13 +23,13 @@ type PostgresResult<
2323
Types extends TypesConstraint,
2424
Models extends ModelsConstraint,
2525
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
26-
> = Omit<
27-
ReturnType<typeof baseDefineContract<SqlFamily, PostgresPack, Types, Models, ExtensionPacks>>,
28-
'target' | 'targetFamily'
29-
> & {
30-
readonly target: PostgresPack['targetId'];
31-
readonly targetFamily: SqlFamily['familyId'];
32-
};
26+
> = ReturnType<
27+
typeof buildBoundContract<
28+
SqlFamily,
29+
PostgresPack,
30+
{ readonly types?: Types; readonly models?: Models; readonly extensionPacks?: ExtensionPacks }
31+
>
32+
>;
3333

3434
type PostgresBaseScaffold<
3535
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
@@ -81,29 +81,18 @@ export function defineContract<
8181
},
8282
): PostgresResult<Types, Models, ExtensionPacks>;
8383

84+
// Implementation — delegates to buildBoundContract which pre-binds family/target,
85+
// carrying zero casts at this layer.
8486
export function defineContract(
85-
scaffold: Omit<ContractInput, 'family' | 'target'>,
87+
definition: PostgresDefinition<TypesConstraint, ModelsConstraint, undefined>,
8688
factory?: (helpers: ComposedAuthoringHelpers<SqlFamily, PostgresPack, undefined>) => {
8789
readonly types?: TypesConstraint;
8890
readonly models?: ModelsConstraint;
8991
},
9092
): PostgresResult<TypesConstraint, ModelsConstraint, undefined> {
91-
const full = {
92-
...scaffold,
93-
family: sqlFamilyPack,
94-
target: postgresPack,
95-
createNamespace: postgresCreateNamespace,
96-
} as ContractInput;
93+
const bound = { ...definition, createNamespace: postgresCreateNamespace };
9794
if (factory !== undefined) {
98-
const { types: _t, models: _m, ...scaffoldOnly } = full;
99-
return baseDefineContract(
100-
scaffoldOnly,
101-
factory as Parameters<typeof baseDefineContract>[1],
102-
) as unknown as PostgresResult<TypesConstraint, ModelsConstraint, undefined>;
95+
return buildBoundContract(sqlFamilyPack, postgresPack, bound, factory);
10396
}
104-
return baseDefineContract(full) as unknown as PostgresResult<
105-
TypesConstraint,
106-
ModelsConstraint,
107-
undefined
108-
>;
97+
return buildBoundContract(sqlFamilyPack, postgresPack, bound);
10998
}

packages/3-extensions/sqlite/src/contract/define-contract.ts

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
ContractInput,
77
ModelLike,
88
} from '@prisma-next/sql-contract-ts/contract-builder';
9-
import { defineContract as baseDefineContract } from '@prisma-next/sql-contract-ts/contract-builder';
9+
import { buildBoundContract } from '@prisma-next/sql-contract-ts/contract-builder';
1010
import { sqliteCreateNamespace } from '@prisma-next/target-sqlite/control';
1111
import sqlitePack from '@prisma-next/target-sqlite/pack';
1212

@@ -20,13 +20,13 @@ type SqliteResult<
2020
Types extends TypesConstraint,
2121
Models extends ModelsConstraint,
2222
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
23-
> = Omit<
24-
ReturnType<typeof baseDefineContract<SqlFamily, SqlitePack, Types, Models, ExtensionPacks>>,
25-
'target' | 'targetFamily'
26-
> & {
27-
readonly target: SqlitePack['targetId'];
28-
readonly targetFamily: SqlFamily['familyId'];
29-
};
23+
> = ReturnType<
24+
typeof buildBoundContract<
25+
SqlFamily,
26+
SqlitePack,
27+
{ readonly types?: Types; readonly models?: Models; readonly extensionPacks?: ExtensionPacks }
28+
>
29+
>;
3030

3131
type SqliteBaseScaffold<
3232
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
@@ -48,10 +48,6 @@ type SqliteScaffold<
4848
ExtensionPacks extends Record<string, ExtensionPackRef<'sql', string>> | undefined,
4949
> = SqliteBaseScaffold<ExtensionPacks>;
5050

51-
const sqliteAuthoringDefaults = {
52-
createNamespace: sqliteCreateNamespace,
53-
} as const;
54-
5551
export function defineContract<
5652
const Types extends TypesConstraint = Record<never, never>,
5753
const Models extends ModelsConstraint = Record<never, never>,
@@ -76,29 +72,18 @@ export function defineContract<
7672
},
7773
): SqliteResult<Types, Models, ExtensionPacks>;
7874

75+
// Implementation — delegates to buildBoundContract which pre-binds family/target,
76+
// carrying zero casts at this layer.
7977
export function defineContract(
80-
scaffold: Omit<ContractInput, 'family' | 'target'>,
78+
definition: SqliteDefinition<TypesConstraint, ModelsConstraint, undefined>,
8179
factory?: (helpers: ComposedAuthoringHelpers<SqlFamily, SqlitePack, undefined>) => {
8280
readonly types?: TypesConstraint;
8381
readonly models?: ModelsConstraint;
8482
},
8583
): SqliteResult<TypesConstraint, ModelsConstraint, undefined> {
86-
const full = {
87-
...scaffold,
88-
...sqliteAuthoringDefaults,
89-
family: sqlFamilyPack,
90-
target: sqlitePack,
91-
} as ContractInput;
84+
const bound = { ...definition, createNamespace: sqliteCreateNamespace };
9285
if (factory !== undefined) {
93-
const { types: _t, models: _m, ...scaffoldOnly } = full;
94-
return baseDefineContract(
95-
{ ...scaffoldOnly, ...sqliteAuthoringDefaults },
96-
factory as Parameters<typeof baseDefineContract>[1],
97-
) as unknown as SqliteResult<TypesConstraint, ModelsConstraint, undefined>;
86+
return buildBoundContract(sqlFamilyPack, sqlitePack, bound, factory);
9887
}
99-
return baseDefineContract(full) as unknown as SqliteResult<
100-
TypesConstraint,
101-
ModelsConstraint,
102-
undefined
103-
>;
88+
return buildBoundContract(sqlFamilyPack, sqlitePack, bound);
10489
}

skills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ defineContract wrapper to eliminate bare casts via a shared bound
1818
contract builder. No extension API or behaviour change; incidental
1919
substrate diff only.
2020
-->
21+
22+
# 0.12 → 0.13 — Extension-author upgrade instructions
23+
24+
No extension-author action required for this transition. The `defineContract` wrappers in the `@prisma-next/postgres` and `@prisma-next/sqlite` packages were refactored to be cast-free via a shared `buildBoundContract` helper in `@prisma-next/sql-contract-ts`; the consumer-facing API and emitted contract shapes are unchanged.

0 commit comments

Comments
 (0)