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
6 changes: 6 additions & 0 deletions .changeset/entity-type-nav-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@baseplate-dev/project-builder-lib': patch
'@baseplate-dev/project-builder-web': patch
---

Refactor entity type URL registration to use a plugin spec with a typed discriminated union navigation target system. Builders now register via `entityTypeUrlWebSpec.register(entityType, builder)` with params typed based on whether the entity has a parent — `parentId` and `parentKey` are required strings for child entity types and `undefined` for root entity types.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
import { getLatestMigrationVersion } from '#src/migrations/index.js';
import { createPluginSpecStore } from '#src/parser/parser.js';
import { deserializeSchemaWithTransformedReferences } from '#src/references/deserialize-schema.js';
import { createDefinitionSchemaParserContext } from '#src/schema/index.js';
import { createProjectDefinitionSchema } from '#src/schema/project-definition.js';

import { ProjectDefinitionContainer } from './project-definition-container.js';
Expand Down Expand Up @@ -52,10 +53,12 @@ export function createTestProjectDefinitionContainer(
const pluginSpecStore = createPluginSpecStore(pluginStore, {
plugins: [],
});
const schema = createProjectDefinitionSchema(
createDefinitionSchemaParserContext({ plugins: pluginSpecStore }),
);
const resolvedRefPayload = deserializeSchemaWithTransformedReferences(
createProjectDefinitionSchema,
schema,
createTestProjectDefinitionInput(input),
{ plugins: pluginSpecStore },
);
return new ProjectDefinitionContainer(
resolvedRefPayload,
Expand All @@ -70,5 +73,6 @@ export function createTestProjectDefinitionContainer(
},
},
pluginSpecStore,
schema,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import type {
FixRefDeletionResult,
ResolvedZodRefPayload,
} from '#src/references/index.js';
import type { ProjectDefinition } from '#src/schema/index.js';
import type {
ProjectDefinition,
ProjectDefinitionSchema,
} from '#src/schema/index.js';

import {
createPluginSpecStore,
Expand All @@ -20,7 +23,10 @@ import {
fixRefDeletions,
serializeSchema,
} from '#src/references/index.js';
import { createProjectDefinitionSchema } from '#src/schema/index.js';
import {
createDefinitionSchemaParserContext,
createProjectDefinitionSchema,
} from '#src/schema/index.js';

/**
* Container for a project definition that includes references and entities.
Expand All @@ -35,18 +41,32 @@ export class ProjectDefinitionContainer {
entities: DefinitionEntity[];
parserContext: SchemaParserContext;
pluginStore: PluginSpecStore;
schema: ProjectDefinitionSchema;

constructor(
config: ResolvedZodRefPayload<ProjectDefinition>,
parserContext: SchemaParserContext,
pluginStore: PluginSpecStore,
schema: ProjectDefinitionSchema,
) {
this.refPayload = config;
this.definition = config.data;
this.references = config.references;
this.entities = config.entities;
this.parserContext = parserContext;
this.pluginStore = pluginStore;
this.schema = schema;
}

/**
* Fetches a DefinitionEntity by its ID, returning undefined if not found.
*
* @param id The ID of the entity to fetch
* @returns The DefinitionEntity, or undefined if not found
*/
entityFromId(id: string | undefined): DefinitionEntity | undefined {
if (!id) return undefined;
return this.entities.find((e) => e.id === id);
}

/**
Expand Down Expand Up @@ -89,11 +109,9 @@ export class ProjectDefinitionContainer {
*/
fixRefDeletions(
setter: (draftConfig: ProjectDefinition) => void,
): FixRefDeletionResult<typeof createProjectDefinitionSchema> {
): FixRefDeletionResult<ProjectDefinition> {
const newDefinition = produce(setter)(this.definition);
return fixRefDeletions(createProjectDefinitionSchema, newDefinition, {
plugins: this.pluginStore,
});
return fixRefDeletions(this.schema, newDefinition);
}

/**
Expand All @@ -102,13 +120,7 @@ export class ProjectDefinitionContainer {
* @returns The serialized contents of the project definition
*/
toSerializedContents(): string {
const serializedContents = serializeSchema(
createProjectDefinitionSchema,
this.definition,
{
plugins: this.pluginStore,
},
);
const serializedContents = serializeSchema(this.schema, this.definition);
return stringifyPrettyStable(serializedContents);
}

Expand All @@ -125,10 +137,14 @@ export class ProjectDefinitionContainer {
): ProjectDefinitionContainer {
const { definition: parsedDefinition, pluginStore } =
parseProjectDefinitionWithReferences(definition, context);
const schema = createProjectDefinitionSchema(
createDefinitionSchemaParserContext({ plugins: pluginStore }),
);
return new ProjectDefinitionContainer(
parsedDefinition,
context,
pluginStore,
schema,
Comment on lines 143 to +147

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep parserContext.pluginStore consistent with pluginStore inside the container.

Both factories pass an updated pluginStore (Lines 147/169) but still pass the original context (Lines 145/168). This can leave this.parserContext.pluginStore out of sync with this.pluginStore.

Suggested fix
   static fromDefinition(
     definition: ProjectDefinition,
     context: SchemaParserContext,
   ): ProjectDefinitionContainer {
     const { definition: parsedDefinition, pluginStore } =
       parseProjectDefinitionWithReferences(definition, context);
     const schema = createProjectDefinitionSchema(
       createDefinitionSchemaParserContext({ plugins: pluginStore }),
     );
+    const parserContext: SchemaParserContext = {
+      ...context,
+      pluginStore,
+    };
     return new ProjectDefinitionContainer(
       parsedDefinition,
-      context,
+      parserContext,
       pluginStore,
       schema,
     );
   }
@@
   static fromSerializedConfig(
     config: unknown,
     context: SchemaParserContext,
   ): ProjectDefinitionContainer {
     const plugins = createPluginSpecStore(context.pluginStore, config);
     const schema = createProjectDefinitionSchema(
       createDefinitionSchemaParserContext({ plugins }),
     );
+    const parserContext: SchemaParserContext = {
+      ...context,
+      pluginStore: plugins,
+    };
     return new ProjectDefinitionContainer(
       deserializeSchemaWithTransformedReferences(schema, config),
-      context,
+      parserContext,
       plugins,
       schema,
     );
   }

Also applies to: 166-170

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/project-builder-lib/src/definition/project-definition-container.ts`
around lines 143 - 147, The constructor call creating ProjectDefinitionContainer
should receive a parserContext whose pluginStore matches the updated pluginStore
being passed into the container; currently the code passes the original context
and the new pluginStore separately, leaving this.parserContext.pluginStore out
of sync. Update the factory calls that invoke new
ProjectDefinitionContainer(...) (the ones building
parsedDefinition/context/pluginStore/schema around ProjectDefinitionContainer)
to clone or replace the incoming context so that parserContext.pluginStore is
set to the same pluginStore object you pass as the pluginStore argument (i.e.,
construct a new context or assign contextWithUpdatedPlugins = { ...context,
pluginStore } and pass that instead). Apply the same change to the other factory
invocation referenced (the second occurrence around the other constructor call)
so both places keep parserContext.pluginStore consistent with pluginStore.

);
}

Expand All @@ -144,14 +160,14 @@ export class ProjectDefinitionContainer {
context: SchemaParserContext,
): ProjectDefinitionContainer {
const plugins = createPluginSpecStore(context.pluginStore, config);
const schema = createProjectDefinitionSchema(
createDefinitionSchemaParserContext({ plugins }),
);
return new ProjectDefinitionContainer(
deserializeSchemaWithTransformedReferences(
createProjectDefinitionSchema,
config,
{ plugins },
),
deserializeSchemaWithTransformedReferences(schema, config),
context,
plugins,
schema,
);
}
}
7 changes: 5 additions & 2 deletions packages/project-builder-lib/src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,13 @@ export function parseProjectDefinitionWithReferences(
} {
const { pluginStore } = context;
const pluginSpecStore = createPluginSpecStore(pluginStore, projectDefinition);
const definitionContext = createDefinitionSchemaParserContext({
plugins: pluginSpecStore,
});
const schema = createProjectDefinitionSchema(definitionContext);
const definition = parseSchemaWithTransformedReferences(
createProjectDefinitionSchema,
schema,
projectDefinition,
{ plugins: pluginSpecStore },
);
return { definition, pluginStore: pluginSpecStore };
}
33 changes: 10 additions & 23 deletions packages/project-builder-lib/src/references/deserialize-schema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import type { z } from 'zod';

import { toposort } from '@baseplate-dev/utils';
import { groupBy, keyBy, uniq } from 'es-toolkit';
import { get, set } from 'es-toolkit/compat';

import type {
DefinitionSchemaCreator,
DefinitionSchemaCreatorOptions,
} from '#src/schema/creator/types.js';
import type { def } from '#src/schema/index.js';

import type { DefinitionEntity, ResolvedZodRefPayload } from './types.js';

import { parseSchemaWithTransformedReferences } from './parse-schema-with-references.js';
Expand All @@ -20,27 +16,18 @@ function referenceToNameParentId(name: string, parentId?: string): string {
* Deserialize a schema with references using the new transform-based approach.
* This function converts human-readable names back to entity IDs.
*
* @template T - The schema creator type
* @param schemaCreator - The schema creator function
* @template T - The Zod schema type
* @param schema - The already-constructed Zod schema
* @param input - The input data with names instead of IDs
* @param options - Options for the schema creator
* @returns The resolved payload with IDs instead of names
*/
export function deserializeSchemaWithTransformedReferences<
T extends DefinitionSchemaCreator,
>(
schemaCreator: T,
export function deserializeSchemaWithTransformedReferences<T extends z.ZodType>(
schema: T,
input: unknown,
options: DefinitionSchemaCreatorOptions,
): ResolvedZodRefPayload<def.InferOutput<T>> {
const payload = parseSchemaWithTransformedReferences(
schemaCreator,
input,
options,
{
skipReferenceNameResolution: true,
},
);
): ResolvedZodRefPayload<z.output<T>> {
const payload = parseSchemaWithTransformedReferences(schema, input, {
skipReferenceNameResolution: true,
});

// Use the same resolution logic as the original function
return resolveReferencesToIds(payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { describe, expect, it } from 'vitest';
import { z } from 'zod';

import { PluginSpecStore } from '#src/plugins/index.js';
import { definitionSchema } from '#src/schema/creator/schema-creator.js';
import {
createDefinitionSchemaParserContext,
definitionSchema,
} from '#src/schema/creator/schema-creator.js';

import { deserializeSchemaWithTransformedReferences } from './deserialize-schema.js';
import { createEntityType } from './types.js';

describe('deserializeSchemaWithTransformedReferences', () => {
const pluginStore = new PluginSpecStore();
const parserContext = createDefinitionSchemaParserContext({
plugins: pluginStore,
});

it('should work with a no-reference object', () => {
const schemaCreator = definitionSchema(() =>
Expand All @@ -18,9 +24,8 @@ describe('deserializeSchemaWithTransformedReferences', () => {
);

const refPayload = deserializeSchemaWithTransformedReferences(
schemaCreator,
schemaCreator(parserContext),
{ test: 'hi' },
{ plugins: pluginStore },
);

expect(refPayload).toMatchObject({
Expand Down Expand Up @@ -53,9 +58,8 @@ describe('deserializeSchemaWithTransformedReferences', () => {
};

const parsedData = deserializeSchemaWithTransformedReferences(
schemaCreator,
schemaCreator(parserContext),
dataInput,
{ plugins: pluginStore },
);

expect(parsedData.data.ref).toEqual(parsedData.data.entity[0].id);
Expand Down Expand Up @@ -86,9 +90,8 @@ describe('deserializeSchemaWithTransformedReferences', () => {
};

const parsedData = deserializeSchemaWithTransformedReferences(
schemaCreator,
schemaCreator(parserContext),
dataInput,
{ plugins: pluginStore },
);

expect(parsedData.data.optionalRef).toBeUndefined();
Expand Down Expand Up @@ -117,9 +120,10 @@ describe('deserializeSchemaWithTransformedReferences', () => {
};

expect(() =>
deserializeSchemaWithTransformedReferences(schemaCreator, dataInput, {
plugins: pluginStore,
}),
deserializeSchemaWithTransformedReferences(
schemaCreator(parserContext),
dataInput,
),
).toThrow('Unable to resolve reference');
});

Expand Down Expand Up @@ -159,9 +163,8 @@ describe('deserializeSchemaWithTransformedReferences', () => {
};

const parsedData = deserializeSchemaWithTransformedReferences(
schemaCreator,
schemaCreator(parserContext),
dataInput,
{ plugins: pluginStore },
);

expect(parsedData.data.ref).toEqual(parsedData.data.entity[2].id);
Expand Down
Loading