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,4 +1,5 @@
import { isArrayEqual } from '@internal/utils/array-equal';
import { blindCast } from '@internal/utils/casts';
import { ifDefined } from '@internal/utils/defined';
import type { JsonObject } from '@internal/utils/json';
import { matchesPathPattern, type PathPattern } from './canonicalization-path-match';
Expand Down Expand Up @@ -48,6 +49,14 @@ const DOMAIN_MODEL_RELATIONS_PATTERN = [
'*',
'relations',
] as const satisfies PathPattern;
const DOMAIN_MODEL_FIELDS_PATTERN = [
'domain',
'namespaces',
'*',
'models',
'*',
'fields',
] as const satisfies PathPattern;
const DOMAIN_MODEL_STORAGE_PATTERN = [
'domain',
'namespaces',
Expand Down Expand Up @@ -142,6 +151,7 @@ function omitDefaults(
'defaults',
]);
const isExtensionNamespace = currentPath.length === 2 && currentPath[0] === 'extensions';
const isModelFields = matchesPathPattern(currentPath, DOMAIN_MODEL_FIELDS_PATTERN);
const isModelRelations = matchesPathPattern(currentPath, DOMAIN_MODEL_RELATIONS_PATTERN);
const isModelStorage = matchesPathPattern(currentPath, DOMAIN_MODEL_STORAGE_PATTERN);

Expand All @@ -161,6 +171,7 @@ function omitDefaults(
!isRequiredMeta &&
!isRequiredExecutionDefaults &&
!isExtensionNamespace &&
!isModelFields &&
!isModelRelations &&
!isModelStorage &&
!isNullableField &&
Expand All @@ -186,9 +197,10 @@ function sortObjectKeys(obj: unknown): unknown {
}

const sorted: Record<string, unknown> = {};
const keys = Object.keys(obj).sort();
const record = Object.fromEntries(Object.entries(obj));
const keys = Object.keys(record).sort();
for (const key of keys) {
sorted[key] = sortObjectKeys((obj as Record<string, unknown>)[key]);
sorted[key] = sortObjectKeys(record[key]);
}

return sorted;
Expand Down Expand Up @@ -266,14 +278,17 @@ export function canonicalizeContractToObject(
...ifDefined('defaultControlPolicy', serialized['defaultControlPolicy']),
meta: serialized['meta'],
};
const withDefaultsOmitted = omitDefaults(normalized, [], options.shouldPreserveEmpty) as Record<
string,
unknown
>;
const withDefaultsOmitted = blindCast<
Record<string, unknown>,
'omitDefaults preserves the record shape of its normalized contract input'
>(omitDefaults(normalized, [], options.shouldPreserveEmpty));
const withSortedStorage = options.sortStorage
? { ...withDefaultsOmitted, storage: options.sortStorage(withDefaultsOmitted['storage']) }
: withDefaultsOmitted;
const withSortedKeys = sortObjectKeys(withSortedStorage) as Record<string, unknown>;
const withSortedKeys = blindCast<
Record<string, unknown>,
'sortObjectKeys preserves the record shape of its contract input'
>(sortObjectKeys(withSortedStorage));
return orderTopLevel(withSortedKeys);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { canonicalizeContractToObject } from '@internal/contract/hashing';
import type { Contract } from '@internal/contract/types';
import { crossRef } from '@internal/contract/types';
import type { CodecLookup } from '@internal/framework-components/codec';
import { UNBOUND_NAMESPACE_ID } from '@internal/framework-components/ir';
import { MongoContractSchema } from '@internal/mongo-contract';
import { mongoContractCanonicalizationHooks } from '@internal/mongo-contract/canonicalization-hooks';

function modelsOf(ir: Contract): Record<string, unknown> {
return ir.domain.namespaces[UNBOUND_NAMESPACE_ID]!.models;
Expand All @@ -10,6 +13,7 @@ function modelsOf(ir: Contract): Record<string, unknown> {
import { buildSymbolTable, type SymbolTable } from '@internal/psl-parser';
import type { SourceFile } from '@internal/psl-parser/syntax';
import { parse } from '@internal/psl-parser/syntax';
import type { JsonObject } from '@internal/utils/json';
import { describe, expect, it } from 'vitest';
import { interpretPslDocumentToMongoContract } from '../src/interpreter';

Expand Down Expand Up @@ -130,6 +134,38 @@ describe('interpretPslDocumentToMongoContract — polymorphism', () => {
expect(modelsOf(ir)['Bug']).toMatchObject({ base: crossRef('Task') });
});

it('preserves empty fields on a field-less variant through canonicalization', () => {
const ir = interpretOk(`
model Post {
id ObjectId @id @map("_id")
title String
kind String

@@discriminator(kind)
@@map("posts")
}

model Note {
@@base(Post, "note")
}
`);

expect(modelsOf(ir)['Note']).toHaveProperty('fields', {});

const canonical = canonicalizeContractToObject(ir, {
serializeContract: (contract) => JSON.parse(JSON.stringify(contract)) as JsonObject,
shouldPreserveEmpty: mongoContractCanonicalizationHooks.shouldPreserveEmpty,
});
const note = (
canonical['domain'] as {
namespaces: Record<string, { models: Record<string, unknown> }>;
}
).namespaces[UNBOUND_NAMESPACE_ID]!.models['Note'];

expect(note).toHaveProperty('fields', {});
expect(() => MongoContractSchema.assert(canonical)).not.toThrow();
});

it('variant inherits base collection (single-collection)', () => {
const ir = interpretOk(`
model Task {
Expand Down