Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PrismaNextConfig } from '@internal/config-loader';
import * as configLoader from '@internal/config-loader';
import type { AttributeSpecContext } from '@internal/psl-parser';
import { assembleAttributeSpecs, modelAttribute } from '@internal/psl-parser';
import { assembleAttributeSpecs, fieldAttribute, modelAttribute } from '@internal/psl-parser';
import { ok } from '@internal/utils/result';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { resolveConfigInputs } from '../src/config-resolution';
Expand All @@ -10,6 +10,19 @@ import { runPipeline } from '../src/pipeline';
vi.mock('@internal/config-loader', { spy: true });

const rlsSpec = modelAttribute('rls', {});
const markerSpec = fieldAttribute('marker', {});

const familyPack = {
kind: 'family',
id: 'demo-family',
version: '0.0.1',
authoring: {
attributeSpecs: {
model: {},
field: { marker: () => markerSpec },
},
},
};

const targetPack = {
kind: 'target',
Expand All @@ -31,7 +44,7 @@ const targetPack = {

function pslProjectConfig(): PrismaNextConfig {
return {
family: { kind: 'family', id: 'demo-family', version: '0.0.1' },
family: familyPack,
target: targetPack,
extensions: [],
contract: {
Expand Down Expand Up @@ -68,6 +81,35 @@ describe('assembled attribute specs are consumable from a resolved project', ()
expect(Object.keys(contributions.modelAttributes)).toEqual(['security']);
});

it('enumerates a family-registered field attribute and invokes its factory', async () => {
vi.spyOn(configLoader, 'loadConfig').mockResolvedValue(
ok({ config: pslProjectConfig(), diagnostics: [] }),
);

const result = await resolveConfigInputs('/abs/prisma.config.ts');
const interpretation = result.interpretation;
expect(interpretation).toBeDefined();
if (interpretation === undefined) return;

const pipeline = runPipeline('model Widget {\n id Int @id\n}\n', result.controlStack);
const model = pipeline.symbolTable.topLevel.models['Widget'];
const field = model?.fields['id'];
expect(field).toBeDefined();
if (model === undefined || field === undefined) return;

const specs = assembleAttributeSpecs(interpretation.context.authoringContributions);
expect(Object.keys(specs.field)).toEqual(['marker']);

const spec = specs.field['marker']?.({
symbols: pipeline.symbolTable,
model,
field,
controlMutationDefaults:
interpretation.context.controlMutationDefaults.defaultFunctionRegistry,
});
expect(spec).toMatchObject({ name: 'marker', level: 'field' });
});

it('invokes the enumerated factory to obtain the attribute spec', async () => {
vi.spyOn(configLoader, 'loadConfig').mockResolvedValue(
ok({ config: pslProjectConfig(), diagnostics: [] }),
Expand Down
1 change: 1 addition & 0 deletions packages/2-sql/2-authoring/contract-psl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"types": "./dist/index.d.mts",
"exports": {
".": "./dist/index.mjs",
"./attribute-specs": "./dist/attribute-specs.mjs",
"./provider": "./dist/provider.mjs",
"./package.json": "./package.json"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { sqlAttributeSpecs } from '../sql-attribute-specs';
81 changes: 43 additions & 38 deletions packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
import { invariant } from '@internal/utils/assertions';
import { blindCast } from '@internal/utils/casts';
import { ifDefined } from '@internal/utils/defined';
import { InternalError } from '@internal/utils/internal-error';
import { notOk, ok, type Result } from '@internal/utils/result';
import { contractError } from './contract-errors';

Expand Down Expand Up @@ -108,16 +109,10 @@ import {
validateBackrelationFieldAttributes,
} from './psl-relation-resolution';
import {
baseModelSpec,
checkModelSpec,
controlModelSpec,
discriminatorModelSpec,
findModelAttributeNode,
idModelSpec,
indexModelSpec,
interpretModelAttribute,
PSL_CHECK_ON_STI_VARIANT,
uniqueModelSpec,
sqlAttributeSpecs,
} from './sql-attribute-specs';

export interface InterpretPslDocumentToSqlContractInput {
Expand Down Expand Up @@ -696,6 +691,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult

const resolvedFields = collectResolvedFields({
model,
symbolTable: input.symbolTable,
mapping,
enumTypeDescriptors: input.enumTypeDescriptors,
namedTypeDescriptors: input.namedTypeDescriptors,
Expand Down Expand Up @@ -837,6 +833,37 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult

const modelAttributeNodes = Array.from(model.node.attributes());
for (const [attributeIndex, modelAttribute] of model.attributes.entries()) {
if (
!Object.hasOwn(sqlAttributeSpecs.model, modelAttribute.name) &&
!input.modelAttributesByName.has(modelAttribute.name)
) {
const uncomposedNamespace = checkUncomposedNamespace(
modelAttribute.name,
input.composedExtensions,
{
familyId: input.familyId,
targetId: input.targetId,
authoringContributions: input.authoringContributions,
},
);
if (uncomposedNamespace) {
reportUncomposedNamespace({
subjectLabel: `Attribute "@@${modelAttribute.name}"`,
namespace: uncomposedNamespace,
sourceId,
span: modelAttribute.span,
diagnostics,
});
continue;
}
diagnostics.push({
code: 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE',
message: `Model "${model.name}" uses unsupported attribute "@@${modelAttribute.name}"`,
sourceId,
span: modelAttribute.span,
});
continue;
}
if (modelAttribute.name === 'map') {
continue;
}
Expand All @@ -862,7 +889,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
}
const parsed = interpretModelAttribute({
node,
spec: controlModelSpec,
spec: sqlAttributeSpecs.model.control(),
model,
sourceFile: input.sourceFile,
sourceId,
Expand Down Expand Up @@ -900,7 +927,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
}
const parsed = interpretModelAttribute({
node,
spec: idModelSpec,
spec: sqlAttributeSpecs.model.id(),
model,
sourceFile: input.sourceFile,
sourceId,
Expand Down Expand Up @@ -946,7 +973,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
}
const parsed = interpretModelAttribute({
node,
spec: uniqueModelSpec,
spec: sqlAttributeSpecs.model.unique(),
model,
sourceFile: input.sourceFile,
sourceId,
Expand Down Expand Up @@ -980,7 +1007,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
}
const parsed = interpretModelAttribute({
node,
spec: indexModelSpec,
spec: sqlAttributeSpecs.model.index(),
model,
sourceFile: input.sourceFile,
sourceId,
Expand Down Expand Up @@ -1041,7 +1068,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
}
const parsed = interpretModelAttribute({
node,
spec: checkModelSpec,
spec: sqlAttributeSpecs.model.check(),
model,
sourceFile: input.sourceFile,
sourceId,
Expand Down Expand Up @@ -1124,31 +1151,9 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
slot[lowered.key] = lowered.entity;
continue;
}
const uncomposedNamespace = checkUncomposedNamespace(
modelAttribute.name,
input.composedExtensions,
{
familyId: input.familyId,
targetId: input.targetId,
authoringContributions: input.authoringContributions,
},
throw new InternalError(
`Model attribute "@@${modelAttribute.name}" is registered but has no interpreter branch`,
);
if (uncomposedNamespace) {
reportUncomposedNamespace({
subjectLabel: `Attribute "@@${modelAttribute.name}"`,
namespace: uncomposedNamespace,
sourceId,
span: modelAttribute.span,
diagnostics,
});
continue;
}
diagnostics.push({
code: 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE',
message: `Model "${model.name}" uses unsupported attribute "@@${modelAttribute.name}"`,
sourceId,
span: modelAttribute.span,
});
}

const resultFkRelationMetadata: FkRelationMetadata[] = [];
Expand Down Expand Up @@ -1643,7 +1648,7 @@ function collectPolymorphismDeclarations(
if (discriminatorNode !== undefined) {
const parsed = interpretModelAttribute({
node: discriminatorNode,
spec: discriminatorModelSpec,
spec: sqlAttributeSpecs.model.discriminator(),
model,
sourceFile,
sourceId,
Expand All @@ -1669,7 +1674,7 @@ function collectPolymorphismDeclarations(
if (baseNode !== undefined) {
const parsed = interpretModelAttribute({
node: baseNode,
spec: baseModelSpec,
spec: sqlAttributeSpecs.model.base(),
model,
sourceFile,
sourceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
ModelSymbol,
PslSpan,
ResolvedTypeConstructorCall,
SymbolTable,
} from '@internal/psl-parser';
import type { SourceFile } from '@internal/psl-parser/syntax';

Expand All @@ -41,9 +42,10 @@ import { lowerDefaultFunctionWithRegistry } from './default-function-registry';

import { mapPslHelperArgs } from './psl-authoring-arguments';
import {
buildDefaultSpec,
fieldSpecContext,
findFieldAttributeNode,
interpretFieldAttribute,
sqlAttributeSpecs,
} from './sql-attribute-specs';

export type ColumnDescriptor = {
Expand Down Expand Up @@ -696,23 +698,27 @@ export function lowerDefaultForField(input: {
readonly fieldName: string;
readonly field: FieldSymbol;
readonly model: ModelSymbol;
readonly symbolTable: SymbolTable;
readonly sourceFile: SourceFile;
readonly columnDescriptor: ColumnDescriptor;
readonly generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>;
readonly sourceId: string;
readonly defaultFunctionRegistry: ControlMutationDefaultRegistry;
readonly diagnostics: ContractSourceDiagnostic[];
readonly isList?: boolean;
}): {
readonly defaultValue?: ColumnDefault;
readonly executionDefaults?: ExecutionMutationDefaultPhases;
} {
const node = findFieldAttributeNode(input.field, 'default');
if (node === undefined) return {};
const spec = buildDefaultSpec({
isList: input.isList ?? false,
registry: input.defaultFunctionRegistry,
});
const spec = sqlAttributeSpecs.field.default(
fieldSpecContext({
symbols: input.symbolTable,
model: input.model,
field: input.field,
controlMutationDefaults: input.defaultFunctionRegistry,
}),
);
const interpreted = interpretFieldAttribute({
node,
spec,
Expand Down
Loading
Loading