Skip to content
Merged
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/mongo-demo/src/contract.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion examples/mongo-demo/src/contract.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions examples/mongo-demo/test/enum-value-set.types.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Acceptance: a Mongo enum document field types as the value union on BOTH the emitted
* `contract.d.ts` path (`FieldOutputTypes` / ORM `DefaultModelRow`) AND the no-emit `typeof
* contract` path (the domain enum's value union via `NamespacedEnums`), and the two agree — the
* same shape SQL produces (`FieldOutputTypes[<ns>][<Model>][<field>]` narrows to the value union).
*
* The emit side consumes the real emitted `contract.d.ts` (emit-then-consume), not `typeof
* contract`, so a divergence between authoring and emission is caught.
*/

import type { EnumValues, NamespacedEnums } from '@prisma-next/contract/enum-accessor';
import type { DefaultModelRow } from '@prisma-next/mongo-orm';
import { expectTypeOf, test } from 'vitest';
import type { Contract, FieldOutputTypes } from '../src/contract';

const ROLE_UNION = ['admin', 'author', 'reader'] as const;
type RoleUnion = (typeof ROLE_UNION)[number];

// Emit side: the emitted FieldOutputTypes narrows the enum field to the value union.
type EmittedRole = FieldOutputTypes['__unbound__']['User']['role'];

// Emit side (consumer): the ORM read row derives the same union from the emitted contract.
type OrmRole = DefaultModelRow<Contract, 'User'>['role'];

// No-emit side: the value union carried by the domain enum (`typeof contract`, via db.enums).
type NoEmitRoleValues = EnumValues<NamespacedEnums<Contract>['__unbound__']['UserRole']>;

test('emit: FieldOutputTypes types the enum field as the value union, not string', () => {
expectTypeOf<EmittedRole>().toEqualTypeOf<RoleUnion>();
expectTypeOf<EmittedRole>().not.toEqualTypeOf<string>();
});

test('emit: the ORM read row narrows the enum field to the value union', () => {
expectTypeOf<OrmRole>().toEqualTypeOf<RoleUnion>();
expectTypeOf<OrmRole>().not.toEqualTypeOf<string>();
});

test('no-emit: the domain enum value union equals the same value union', () => {
expectTypeOf<NoEmitRoleValues>().toEqualTypeOf<RoleUnion>();
});

test('emit and no-emit agree: the emitted field type equals the no-emit value union', () => {
expectTypeOf<EmittedRole>().toEqualTypeOf<NoEmitRoleValues>();
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/retail-store/src/contract.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion examples/retail-store/src/contract.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions packages/1-framework/1-core/errors/src/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,27 @@ export function errorConfigValidation(
// Generic Error
// ============================================================================

/**
* An enum declares a codecId that no component in the contract's pack stack provides,
* so its member values cannot be encoded. Thrown by both authoring paths (TS `defineContract`
* and PSL interpretation) when the codec lookup built from the contract's packs has no
* descriptor for the codecId.
*/
export function errorEnumCodecNotInPackStack(options: {
readonly codecId: string;
}): CliStructuredError {
return new CliStructuredError(
'4016',
`Enum codec "${options.codecId}" is not part of the contract's pack stack`,
{
domain: 'CON',
why: `An enum uses codec "${options.codecId}", but no family, target, or extension pack in the contract provides it.`,
fix: "Use a codec provided by the contract's target/extension packs, or add the pack that supplies this codec.",
meta: { codecId: options.codecId },
},
);
}

/**
* Generic unexpected error.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
errorContractValidationFailed,
errorDatabaseConnectionRequired,
errorDriverRequired,
errorEnumCodecNotInPackStack,
errorFamilyReadMarkerSqlRequired,
errorFileNotFound,
errorInvalidOutputFormat,
Expand Down
10 changes: 10 additions & 0 deletions packages/1-framework/1-core/errors/test/control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
errorContractValidationFailed,
errorDatabaseConnectionRequired,
errorDriverRequired,
errorEnumCodecNotInPackStack,
errorFamilyReadMarkerSqlRequired,
errorFileNotFound,
errorInvalidOutputFormat,
Expand Down Expand Up @@ -440,6 +441,15 @@ describe('Config Errors', () => {
expect(envelope.code).toBe('PN-CLI-4015');
expect(error.message).toMatch(/--format pretty.*--json/i);
});

it('errorEnumCodecNotInPackStack produces PN-CON-4016', () => {
const error = errorEnumCodecNotInPackStack({ codecId: 'mongo/string@1' });
const envelope = error.toEnvelope();
expect(error.code).toBe('4016');
expect(envelope.code).toBe('PN-CON-4016');
expect(error.message).toContain('mongo/string@1');
expect(error.meta).toEqual({ codecId: 'mongo/string@1' });
});
});

describe('Generic Error', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ export function extractCodecLookup(
for (const descriptor of descriptors) {
const codecTypes = descriptor.types?.codecTypes;
const descriptorId = descriptor.id;
// Descriptor-side metadata is the single source of truth for `targetTypes` / `meta` / `renderOutputType`. Every contributor ships a `codecDescriptors` list on `types.codecTypes`.
// Descriptor-side metadata is the single source of truth for `targetTypes` / `meta` / `renderOutputType`. A component contributes its codecs by listing `codecDescriptors` on `types.codecTypes`; each codecId has exactly one contributor across the stack.
for (const codecDescriptor of codecTypes?.codecDescriptors ?? []) {
assertUniqueCodecOwner({
codecId: codecDescriptor.codecId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ export const StorageCollectionSchema = type({
'control?': ControlPolicySchema,
});

export const StorageValueSetSchema = type({
'+': 'reject',
kind: "'valueSet'",
values: type('string | number | boolean | null | unknown[] | Record<string, unknown>')
.array()
.readonly(),
});

function collectionEntrySchema(fragments?: ReadonlyMap<string, Type<unknown>>): Type<unknown> {
if (fragments === undefined || fragments.size === 0) {
return StorageCollectionSchema;
Expand Down Expand Up @@ -389,6 +397,7 @@ export function createMongoNamespaceEnvelopeSchema(
'kind?': 'string',
entries: type({
'collection?': type({ '[string]': collectionEntrySchema(fragments) }),
'valueSet?': type({ '[string]': StorageValueSetSchema }),
}),
}).narrow((ns, ctx) => {
if (typeof ns !== 'object' || ns === null || Array.isArray(ns)) {
Expand Down
Loading
Loading