Skip to content

Conversation

@kingston
Copy link
Collaborator

@kingston kingston commented Feb 13, 2025

  • Rewrite of the sync / save flow of project definition to enable clearer flow and async saving
  • Also refactor nameRefs to name resolvers for zod ref payloads

Summary by CodeRabbit

  • New Features

    • Enhanced project management with a modernized interface for saving, syncing, and deleting project definitions.
    • Improved Auth0 integration for user and role management, plus refined plugin enabling/disabling with immediate user feedback.
    • Introduced new functions for downloading, uploading, and listening for changes to project definition files.
  • Improvements

    • Streamlined form behavior that prevents navigation with unsaved changes and disables inputs during processing.
    • Upgraded backend APIs and error handling for smoother project syncing, version detection, and overall stability, supported by updated testing and dependency libraries.
    • Enhanced error handling and user feedback mechanisms across various components.

@linear
Copy link

linear bot commented Feb 13, 2025

ENG-469 When syncing and saving, recently modified error shows up

If you sync and then save, we show a recently modified error toast

@changeset-bot
Copy link

changeset-bot bot commented Feb 13, 2025

🦋 Changeset detected

Latest commit: e0a068f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@halfdomelabs/ui-components Patch
@halfdomelabs/project-builder-server Patch
@halfdomelabs/project-builder-lib Patch
@halfdomelabs/project-builder-web Patch
@halfdomelabs/baseplate-plugin-storage Patch
@halfdomelabs/project-builder-cli Patch
@halfdomelabs/project-builder-common Patch
@halfdomelabs/project-builder-test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2025

Walkthrough

The changes span multiple packages and affect dependency updates, type definitions, method signatures, and file structures. Notable updates include dependency patches, migration from legacy payload types to new ResolvedZodRefPayload, and removal of deprecated classes (e.g. ParsedProjectDefinition). API context and router implementations have been refactored in the server package, while the web client sees extensive updates in component naming, form handling, and state management (using new saveDefinitionWithFeedback variants). New utilities and unit tests have been added, and several error handling mechanisms (including custom error types) have been introduced.

Changes

File(s) Change Summary
.changeset/afraid-foxes-build.md Update dependency patch for @halfdomelabs/ui-components and upgrade sonner to v1.7.4.
packages/.../project-builder-lib/src/(compiler, definition, parser, migrations, references, schema, tools/model-merger)* Refactored method signatures and type declarations; replaced ZodRefPayload with ResolvedZodRefPayload; removed ParsedProjectDefinition; added new migration, reference, and model diff/patch utilities; updated entity and name resolution logic.
packages/.../project-builder-server/src/(api, compiler, runner, service, plugin, context)* Reorganized API context and router definitions (e.g. replaced createContext with createContextBuilder, removed createAppRouter); updated backend compiler modules and service file handling (e.g. renaming file payloads); enhanced error handling with new error types.
packages/.../project-builder-web/src/(app, hooks, pages, components, services)* Renamed and refactored UI components and hooks (e.g. ClientVersionProvider, ProjectDefinitionProvider); updated project definition state management and form submission processes using new saveDefinitionWithFeedback methods; improved unsaved changes handling and adjusted import paths; schema and JSON structure changes (e.g. modelRef, featureRef).
packages/.../project-builder-web/package.json, public/preview-app.json Added testing libraries (e.g. Testing Library packages, jsdom) and updated JSON schema naming conventions alongside schema version increments.
packages/.../project-builder-lib/src/references/(ref-builder.unit.test.ts, resolve-zod-ref-payload-names.unit.test.ts)*, packages/.../services/(hot-loader.ts)* Introduced new unit tests for reference utilities and added a new hot-loader subscription function to handle Vite events.
packages/.../project-builder-web/src/services/api/(errors, plugins, project-definition, projects, sync, version, config)* Added new API service functions for downloading, uploading, and listening to project definition changes; introduced a new error class (ProjectNotFoundError) and helper functions; re-exported modules for consolidated access.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ContextBuilder as createContextBuilder
    participant API as Service Lookup
    Client->>ContextBuilder: Request context (req, res)
    ContextBuilder->>ContextBuilder: Build context with BaseplateApiContext
    ContextBuilder->>API: getApi(projectId)
    API-->>ContextBuilder: Return service instance or undefined
    alt Service found
        ContextBuilder->>Client: Return context with service instance
    else Service not found
        ContextBuilder->>Client: Throw TRPCError("NOT_FOUND")
    end
Loading
sequenceDiagram
    participant User
    participant Provider as ProjectDefinitionProvider
    participant Manager as ProjectDefinitionFileManager
    participant API as Remote Service
    User->>Provider: Submit updated project definition
    Provider->>Manager: Upload new definition with old hash
    Manager->>API: Call uploadProjectDefinition(id, newContents, oldHash)
    API-->>Manager: Return updated file payload
    Manager-->>Provider: New project definition payload
    Provider->>User: Update UI and show success message
Loading
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🔭 Outside diff range comments (1)
packages/project-builder-web/src/pages/plugins/home.page.tsx (1)

77-79: ⚠️ Potential issue

Fix incorrect array usage in plugin rendering.

The plugin rendering sections are using the full plugins array instead of the filtered arrays installedPlugins and uninstalledPlugins.

-          {plugins.map((plugin) => (
+          {installedPlugins.map((plugin) => (
             <PluginCard key={plugin.id} plugin={plugin} isActive />
           ))}

-          {plugins.map((plugin) => (
+          {uninstalledPlugins.map((plugin) => (
             <PluginCard key={plugin.id} plugin={plugin} isActive={false} />
           ))}

Also applies to: 85-87

🧹 Nitpick comments (50)
packages/project-builder-web/src/pages/data/models/_hooks/useEditedModelConfig.tsx (1)

69-82: Consider adding JSDoc documentation for better maintainability.

The hook implementation is solid, but could benefit from JSDoc comments explaining:

  • The purpose of the hook
  • The expected shape of the selector function
  • Example usage

Example documentation:

/**
 * Hook to access and select data from the edited model configuration.
 * Must be used within an EditedModelContextProvider.
 *
 * @param selector - Function to select specific data from the model config
 * @returns Selected data from the model config
 * @throws {Error} If used outside of EditedModelContextProvider
 *
 * @example
 * const name = useEditedModelConfig(model => model.name);
 */
export function useEditedModelConfig<T>(
  selector: (model: ModelConfig) => T,
): T {
  // ... existing implementation
}
packages/project-builder-web/src/pages/plugins/plugin-config.page.tsx (1)

72-86: Consider adding error handling for save failures.

While the success path is well-handled with feedback and navigation, there's no explicit handling of save failures. Consider adding an onError callback to handle failure scenarios gracefully.

 saveDefinitionWithFeedbackSync(
   (draft) => {
     draft.plugins = (draft.plugins ?? []).filter(
       (p) => p.id !== metadata.id,
     );
   },
   {
     successMessage: `Disabled ${metadata.displayName}!`,
     onSuccess: () => {
       navigate('/plugins');
     },
+    onError: (error) => {
+      // Handle the error appropriately
+      console.error('Failed to disable plugin:', error);
+    },
   },
 );
packages/project-builder-web/src/pages/apps/edit/admin/AdminGeneralForm.tsx (3)

42-48: Consider adding type annotation for better code clarity.

The submit handler implementation is clean and functional. Consider adding a type annotation for the draft config parameter to improve code clarity and maintainability.

- saveDefinitionWithFeedback((draftConfig) => {
+ saveDefinitionWithFeedback((draftConfig: typeof definition) => {

52-55: Consider adding null coalescing for role mapping.

To handle cases where definition.auth might be undefined, consider adding null coalescing to provide a default empty array.

- const roleOptions = definition.auth?.roles.map((role) => ({
+ const roleOptions = definition.auth?.roles?.map((role) => ({

58-81: Consider extracting string literals to constants.

The form UI is well-structured with proper disabled state handling. Consider extracting string literals (labels, etc.) to constants for better maintainability and potential internationalization.

+ const FORM_LABELS = {
+   NAME: 'Name',
+   PACKAGE_LOCATION: 'Package Location (optional) e.g. packages/web',
+   ALLOWED_ROLES: 'Allowed Roles?',
+   SAVE: 'Save',
+ } as const;

  return (
    <div className={clsx('', className)}>
      <form onSubmit={onSubmit} className="space-y-4">
        <TextInput.LabelledController
-         label="Name"
+         label={FORM_LABELS.NAME}
          control={control}
          name="name"
        />
        <TextInput.LabelledController
-         label="Package Location (optional) e.g. packages/web"
+         label={FORM_LABELS.PACKAGE_LOCATION}
          control={control}
          name="packageLocation"
        />
        {roleOptions && (
          <CheckedArrayInput.LabelledController
-           label="Allowed Roles?"
+           label={FORM_LABELS.ALLOWED_ROLES}
            control={control}
            options={roleOptions}
            name="allowedRoles"
          />
        )}
        <Button type="submit" disabled={isSavingDefinition}>
-         Save
+         {FORM_LABELS.SAVE}
        </Button>
packages/project-builder-web/src/pages/settings/hierarchy.tsx (2)

43-48: Consider combining reference check with the save operation.

There's a potential race condition between the reference check and the actual deletion. If the project definition changes between these operations, the reference check results might be outdated.

Consider moving the reference check into the save operation:

-    // try deleting the feature
-    const result = definitionContainer.fixRefDeletions(deleteFeature);
-    if (result.type === 'failure') {
-      showRefIssues({ issues: result.issues });
-      return;
-    }
+    const checkAndDelete = (draftConfig: ProjectDefinition): void => {
+      const result = definitionContainer.checkRefDeletions(draftConfig, deleteFeature);
+      if (result.type === 'failure') {
+        throw new Error('Reference check failed');
+      }
+      deleteFeature(draftConfig);
+    };

     requestConfirm({
       title: 'Delete Feature',
       content: `Are you sure you want to delete ${feature.name}?`,
       onConfirm: () => {
-        saveDefinitionWithFeedbackSync(deleteFeature, {
+        saveDefinitionWithFeedbackSync(checkAndDelete, {
           successMessage: `Feature ${feature.name} removed`,
+          onError: (error) => {
+            if (error.message === 'Reference check failed') {
+              showRefIssues({ issues: result.issues });
+            }
+          }
         });
       },
     });

126-132: Enhance accessibility with ARIA labels and keyboard support.

The button lacks proper accessibility attributes for screen readers and keyboard navigation.

Apply these improvements:

     <button
       className="mr-1 flex w-full flex-row items-center space-x-3"
+      aria-label={`Edit feature ${featureDisplayName}`}
+      role="button"
+      tabIndex={0}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          setFeatureToEdit(feature);
+          setShowFeatureForm(true);
+        }
+      }}
       onClick={() => {
         setFeatureToEdit(feature);
         setShowFeatureForm(true);
       }}
     >
packages/project-builder-web/src/pages/apps/edit/admin/crud/AdminCrudSectionForm.tsx (1)

33-36: LGTM! Consider adding type safety.

The direct access to definition.models simplifies the code. Consider adding type annotations for better type safety:

-  const modelOptions = definition.models.map((model) => ({
+  const modelOptions = definition.models.map((model): { label: string; value: string } => ({
     label: model.name,
     value: model.id,
   }));
packages/project-builder-web/src/pages/data/models/_hooks/useModelForm.ts (1)

129-164: Consider refactoring service method validation to reduce duplication.

The validation logic for create and update methods is nearly identical. Consider extracting this into a reusable function to improve maintainability.

Here's a suggested refactor:

const validateServiceMethod = (
  method: { enabled?: boolean; fields?: any[]; transformerNames?: any[] },
  methodName: string
): boolean => {
  if (method?.enabled) {
    if (!method.fields?.length && !method.transformerNames?.length) {
      toast.error(
        `${methodName} method must have at least one field or transformer.`
      );
      return false;
    }
  }
  return true;
};

// Usage:
if (!validateServiceMethod(service.create, 'Create')) return;
if (!validateServiceMethod(service.update, 'Update')) return;
packages/project-builder-server/src/api/context.ts (1)

7-24: Consider using a map-like structure for frequent service lookups.

This implementation works fine for smaller numbers of services. However, if apiContext.services grows large or if lookups happen frequently, using a dictionary or mapping from projectId to service might be more performant and explicit.

packages/project-builder-server/src/api/projects.ts (1)

8-12: Consider adding JSDoc or comments for ProjectInfo.

Documenting the purpose of each field (e.g., id, name, directory) will improve clarity and maintainability.

packages/project-builder-lib/src/migrations/index.ts (1)

18-26: Validate schemaVersion type for extra safety.

The isMigrateableProjectDefinition guard is a strong check. Optionally, consider confirming that schemaVersion is a number to reduce runtime errors down the line.

return (
  typeof projectDefinition === 'object' &&
  !!projectDefinition &&
  'schemaVersion' in projectDefinition
+  && typeof (projectDefinition as ProjectDefinition).schemaVersion === 'number'
);
packages/project-builder-web/src/app/ClientVersionProvider/ClientVersionProvider.tsx (2)

26-35: Consider async/await for readability.

The fetchVersion function is correct and logically sound. However, using an async/await pattern inside a try/catch can improve readability and more explicitly handle errors:

-  const fetchVersion = useCallback(() => {
-    getVersionInfo()
-      .then((version) => {
-        setClientVersionInfo(version);
-      })
-      .catch((error_: unknown) => {
-        logError(error_);
-        setError(error_ as Error);
-      });
-  }, []);
+  const fetchVersion = useCallback(async () => {
+    try {
+      const version = await getVersionInfo();
+      setClientVersionInfo(version);
+    } catch (error_: unknown) {
+      logError(error_);
+      setError(error_ as Error);
+    }
+  }, []);

45-45: Consider user experience of full reload.

Reloading the entire page when a version changes can disrupt a user's workflow. Depending on release requirements, you might explore in-app notifications or partial refresh to avoid losing user context.

packages/project-builder-lib/src/references/fix-ref-deletions.ts (1)

50-72: Calling parseSchemaWithReferences inside a loop.

  1. This pattern is effective when references shift after each deletion fix.
  2. The return block at lines 60–71 properly splits success vs. aggregated failure results.
  3. Confirm that a single pass is sufficient for typical references and that repeated parsing doesn’t introduce performance concerns for large objects.
packages/project-builder-web/src/app/ProjectDefinitionProvider/ProjectDefinitionProvider.tsx (1)

150-157: Dependency array for useMemo
Ensure that all relevant references are included here. Currently, standard usage, but watch for subtle data flows that might cause stale closures.

packages/project-builder-server/src/service/builder-service.ts (4)

11-16: Consider consolidating imports for readability.
You can combine the imports from @halfdomelabs/utils and @halfdomelabs/utils/node to simplify the import statements.


28-40: Good introduction of a file payload interface.
Storing contents and its hash is an effective way to maintain integrity checks. Consider whether you also need metadata such as file size or timestamps for auditing purposes.


138-146: Initial definition fallback is helpful.
Providing a default JSON structure when the file is missing prevents errors in the rest of the flow. One tiny improvement might be to log when a new definition is created from scratch.


187-217: Write method with hash comparison mitigates overwriting conflicts.
Returning 'original-contents-mismatch' is a clear way to signal that the file was updated externally. Consider whether versioning or more detailed conflict resolution is needed if merges must be handled.

packages/project-builder-web/src/pages/features/auth/index.tsx (2)

46-61: Memoized model change detection.
Using diffModel inside useMemo is efficient. If the user or user role references do not exist, returning early prevents TypeError. Consider logging or showing a warning when references are undefined.


115-115: Computed model options.
Mapping definition.models for user selection is straightforward. If model definitions become large, consider memoizing for performance.

packages/project-builder-lib/src/tools/model-merger/model-merger.ts (1)

174-231: diffModel function is cohesive.
Nicely resolves references for relations, confirms changes in primary key, and returns a structured diff. Consider logging or providing more context if some references are not found in definitionContainer.

packages/project-builder-lib/src/references/ref-builder.ts (2)

28-32: Cross-realm Promise check consideration.
object instanceof Promise may fail if a promise originates from another realm (e.g., an iframe). As a minor enhancement, you might consider checking typeof object?.then === 'function'.


50-67: Interface design is clear.
DefinitionEntityNameResolver is well-structured. If asynchronous name resolution is ever required, consider allowing an async return type.

packages/project-builder-web/src/services/api/sync.ts (1)

5-18: Remove unused payload parameter.

The function documentation mentions a payload parameter, but it's not used in the function signature or implementation.

Apply this diff to remove the unused parameter from the documentation:

 /**
  * Starts a sync operation for a project.
  *
  * @param id - The ID of the project to sync.
- * @param payload - The payload to sync.
  */
packages/project-builder-lib/src/references/parse-schema-with-references.ts (1)

11-27: Consider enhancing error handling.

The function is well-documented and implements a clean parsing flow. However, consider wrapping the parsing operation in a try-catch block to provide more context-specific error messages.

 export function parseSchemaWithReferences<T extends z.ZodTypeAny>(
   schema: T,
   input: unknown,
   options?: ResolveZodRefPayloadNamesOptions,
 ): ResolvedZodRefPayload<z.output<T>> {
+  try {
     const wrapper = ZodRefWrapper.create(schema);
     return resolveZodRefPayloadNames(wrapper.parse(input), options);
+  } catch (error) {
+    throw new Error(`Failed to parse schema with references: ${error.message}`, { cause: error });
+  }
 }
packages/project-builder-web/src/services/api/errors.ts (1)

18-27: Add explicit return type annotation.

The function's return type should be explicitly annotated for better code documentation and type safety.

 export function createProjectNotFoundHandler(
   projectId: string,
-): (err: unknown) => never {
+): (err: unknown) => never | ProjectNotFoundError {
   return (err: unknown) => {
     if (isTRPCClientError(err) && err.data?.code === 'NOT_FOUND') {
       throw new ProjectNotFoundError(projectId);
     }
     throw err;
   };
 }
packages/project-builder-web/src/pages/features/auth/auth-models.ts (1)

34-63: Consider adding input validation for userModelRef.

The createAuth0Models function correctly defines the models and their relationships. However, consider adding validation for the userModelRef parameter to ensure it's not empty or invalid.

 export function createAuth0Models(userModelRef: string): {
   user: ModelDefinitionInput;
   userRole: ModelDefinitionInput;
 } {
+  if (!userModelRef?.trim()) {
+    throw new Error('userModelRef must be a non-empty string');
+  }
   return {
     user: {
       fields: AUTH0_MODEL_FIELDS,
packages/project-builder-web/src/app/ProjectDefinitionProvider/services/parse-project-definition-contents.ts (1)

21-25: Consider improving error message specificity.

The error message for invalid project definition could be more specific about what makes it invalid.

-      throw new UserVisibleError(
-        'The project configuration is not valid JSON object. Please check the file and try again.',
-      );
+      throw new UserVisibleError(
+        'The project configuration is not a valid project definition. Expected a JSON object with required project configuration fields. Please check the file and try again.',
+      );
packages/project-builder-web/src/app/ProjectDefinitionProvider/services/schema-parser-context-manager.ts (1)

33-48: Consider debouncing plugin asset change handler.

The hot reload event handler might trigger frequently during development. Consider debouncing the handler to prevent unnecessary context reloads.

+import debounce from 'lodash/debounce';
+
 listenForPluginAssetsChanges(
   onSchemaParserContextUpdated: (
     schemaParserContext: SchemaParserContext,
   ) => void,
   onError: (error: unknown) => void,
 ): () => void {
-    return subscribeToViteHotReloadEvent('plugin-assets-changed', () => {
+    const debouncedHandler = debounce(() => {
       this.loadSchemaParserContext()
         .then((schemaParserContext) => {
           onSchemaParserContextUpdated(schemaParserContext);
         })
         .catch((error: unknown) => {
           onError(error);
         });
-    });
+    }, 300);
+    return subscribeToViteHotReloadEvent('plugin-assets-changed', debouncedHandler);
 }
packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-project-definition-container.ts (2)

21-52: Consider memoizing the error handler callback.

The implementation is solid, but the error handler could be memoized to prevent unnecessary re-renders.

-  const { projectDefinitionContainer, error } = useMemo(() => {
+  const memoizedErrorHandler = useCallback((err: unknown) => {
+    return {
+      projectDefinitionContainer: undefined,
+      error: err,
+    };
+  }, []);
+
+  const { projectDefinitionContainer, error } = useMemo(() => {
     try {
       if (!schemaParserContext || !projectDefinitionFilePayload) {
         return {
           projectDefinitionContainer: undefined,
           error: undefined,
         };
       }

       const definitionContainer = parseProjectDefinitionContents(
         projectDefinitionFilePayload.contents,
         schemaParserContext,
       );

       return {
         projectDefinitionContainer: {
           container: definitionContainer,
           hash: projectDefinitionFilePayload.hash,
         },
         error: undefined,
       };
     } catch (err) {
-      return {
-        projectDefinitionContainer: undefined,
-        error: err,
-      };
+      return memoizedErrorHandler(err);
     }
   }, [schemaParserContext, projectDefinitionFilePayload]);

54-58: Consider using a logging service instead of console.error.

Direct console logging in production code should be replaced with a proper logging service for better error tracking and management.

   useEffect(() => {
     if (error) {
-      console.error(error);
+      logService.error('Project definition parsing error:', error);
     }
   }, [error]);
packages/project-builder-web/src/pages/apps/new.page.tsx (2)

41-54: Fix typo in success message.

There's a typo in the success toast message.

-          toast.success(`Sucessfully created ${data.name}!`);
+          toast.success(`Successfully created ${data.name}!`);

74-76: Consider adding loading state indicator.

The button is correctly disabled during saving, but users might benefit from visual feedback.

-            <Button type="submit" disabled={isSavingDefinition}>
-              Create
+            <Button type="submit" disabled={isSavingDefinition}>
+              {isSavingDefinition ? 'Creating...' : 'Create'}
             </Button>
packages/project-builder-web/src/pages/data/models/edit/[id]/index.page.tsx (2)

40-42: Address TODO comments regarding validation.

The TODO comments indicate missing validation for field types and SET NULL constraints.

Would you like me to help implement the validation logic for:

  1. Field type and referenced type matching
  2. SET NULL validation for non-optional fields?

51-64: Consider adding form error boundary.

The form is wrapped in an ErrorBoundary but might benefit from a more specific form error handling component.

         <form
           onSubmit={onSubmit}
           className="min-w-[700px] max-w-7xl flex-1 space-y-4 px-4 pb-4"
         >
+          <FormErrorBoundary>
           <ModelFieldsForm control={control} setValue={setValue} />
           <SectionList>
             <ModelRelationsSection control={control} setValue={setValue} />
             <ModelUniqueConstraintsSection
               control={control}
               setValue={setValue}
             />
           </SectionList>
           <FormActionBar form={form} />
+          </FormErrorBoundary>
         </form>
packages/project-builder-web/src/pages/data/models/edit/[id]/_components/fields/ModelFieldTypeInput.tsx (1)

26-34: LGTM! Clean refactor of enum options retrieval.

The change from parsedProject.getEnums() to definition.enums aligns with the PR's objective of improving the project definition flow.

Consider adding type assertion or runtime validation for definition.enums to ensure type safety:

-  const enumOptions: ScalarFieldTypeOption[] = (definition.enums ?? []).map(
+  const enumOptions: ScalarFieldTypeOption[] = ((definition.enums ?? []) as Array<{ name: string; id: string }>).map(
packages/project-builder-web/src/services/api/project-definition.ts (1)

69-80: Consider adding error handling for subscription.

The subscription setup looks good, but consider handling potential subscription errors.

 export function listenForProjectDefinitionChanges(
   id: string,
   onData: (value: ProjectDefinitionFilePayload) => void,
 ): () => void {
   const result = trpc.projects.onProjectJsonChanged.subscribe(
     { id },
-    { onData },
+    {
+      onData,
+      onError: (error) => {
+        console.error('Project definition subscription error:', error);
+      },
+    },
   );
   return () => {
     result.unsubscribe();
   };
 }
packages/project-builder-lib/src/schema/models/transformers/built-in-transformers.ts (1)

66-71: LGTM! Enhanced name resolution for embedded relations.

The use of createDefinitionEntityNameResolver provides a more flexible and maintainable approach to name resolution.

Consider adding JSDoc comments to explain the name resolution strategy:

+/**
+ * Resolves the transformer name based on the foreign relation reference.
+ * This ensures that the transformer name stays in sync with its related entity.
+ */
 getNameResolver: (entity) =>
   createDefinitionEntityNameResolver({
     idsToResolve: { foreignRelation: entity.foreignRelationRef },
     resolveName: (entityNames) => entityNames.foreignRelation,
   }),
packages/project-builder-lib/src/web/hooks/useProjectDefinition.ts (1)

16-20: Consider adding JSDoc for SaveDefinitionWithFeedbackOptions.

The interface would benefit from JSDoc documentation explaining each option's purpose and behavior.

Add documentation like this:

+/**
+ * Options for saving project definition with feedback.
+ *
+ * @property disableDeleteRefDialog - Whether to disable the delete reference confirmation dialog
+ * @property successMessage - Custom success message to show in the toast
+ * @property onSuccess - Callback function to execute after successful save
+ */
 export interface SaveDefinitionWithFeedbackOptions {
   disableDeleteRefDialog?: boolean;
   successMessage?: string;
   onSuccess?: () => void;
 }
packages/project-builder-lib/src/references/resolve-zod-ref-payload-names.ts (1)

52-64: Optimize name resolution with memoization.

The resolveIdToName function could benefit from memoization to avoid redundant resolutions of the same IDs.

+  const resolvedNamesCache = new Map<string, string>();
   function resolveIdToName(id: string): string {
     if (skipReferenceNameResolution) {
       return id;
     }
+    const cached = resolvedNamesCache.get(id);
+    if (cached) {
+      return cached;
+    }
     const entity = resolvedEntitiesById.get(id);
     if (!entity) {
       if (allowInvalidReferences) {
         return id;
       }
       throw new Error(`Could not resolve entity name for id: ${id}`);
     }
-    return entity.name;
+    resolvedNamesCache.set(id, entity.name);
+    return entity.name;
   }
packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-remote-project-definition-contents.ts (2)

26-58: Consider adding error retry logic for network failures.

While the error handling is good, network failures during downloadDefinitionContents could benefit from retry logic, especially in unstable network conditions.

 projectDefinitionFileManager
   .downloadDefinitionContents()
+  .retry({
+    retries: 3,
+    minTimeout: 1000,
+    factor: 2,
+  })
   .then((payload) => {
     setProjectDefinitionFilePayload(payload);
   })

60-79: Consider adding loading state for upload operation.

The upload operation could benefit from a loading state to prevent multiple simultaneous uploads and provide better UX feedback.

 const uploadProjectDefinitionContents = useCallback(
   async (newContents: string) => {
+    const [isUploading, setIsUploading] = useState(false);
     if (!projectDefinitionFileManager) {
       throw new Error('Project definition file manager not found');
     }
+    if (isUploading) {
+      return;
+    }
+    setIsUploading(true);
     try {
       await projectDefinitionFileManager
         .uploadDefinitionContents(newContents)
         .then((newPayload) => {
           setProjectDefinitionFilePayload(newPayload);
         })
         .catch((err: unknown) => {
           if (err instanceof ProjectNotFoundError) {
             resetCurrentProjectId();
           }
           throw err;
         });
+    } finally {
+      setIsUploading(false);
+    }
   },
   [projectDefinitionFileManager, resetCurrentProjectId],
 );
packages/project-builder-web/src/pages/apps/edit/WebAppForm.tsx (2)

31-39: Consider adding optimistic updates.

The form submission could benefit from optimistic updates to improve perceived performance.

 const onSubmit = handleSubmit((data) =>
+  // Optimistically update the UI
+  setProjectDefinitionFilePayload((prev) => ({
+    ...prev,
+    apps: prev.apps.map((app) =>
+      app.id === appConfig.id ? data : app,
+    ),
+  })),
   saveDefinitionWithFeedback((draftConfig) => {
     draftConfig.apps = draftConfig.apps.map((app) =>
       app.id === appConfig.id ? data : app,
     );
   }),
 );

99-101: Add loading indicator to the Save button.

The button's disabled state is good, but a loading indicator would provide better UX feedback.

-<Button type="submit" disabled={isSavingDefinition}>
+<Button 
+  type="submit" 
+  disabled={isSavingDefinition}
+  icon={isSavingDefinition ? <Spinner size="sm" /> : undefined}
+>
   Save
 </Button>
packages/project-builder-lib/src/references/deserialize-schema.ts (2)

18-21: Consider adding validation for skipReferenceNameResolution.

The skipReferenceNameResolution option is crucial for correct deserialization. Consider adding validation to ensure it's always true in this context.

 const payload = parseSchemaWithReferences(schema, input, {
   skipReferenceNameResolution: true,
 });
+
+// Ensure name resolution was skipped
+if (!payload.skipReferenceNameResolution) {
+  throw new Error('Reference name resolution must be skipped for deserialization');
+}

27-44: Consider extracting duplicate ID validation to a separate function.

The duplicate ID validation logic is complex and could benefit from being extracted into a separate function for better maintainability.

+function validateUniqueEntityIds(entities: DefinitionEntity[]): void {
+  const entitiesById = groupBy(entities, (entity) => entity.id);
+  const duplicateEntityIds = Object.values(entitiesById).filter(
+    (e) => e.length > 1,
+  );
+  if (duplicateEntityIds.length > 0) {
+    throw new Error(
+      `Found multiple duplicate entity IDs: ${duplicateEntityIds
+        .map(
+          (ents) =>
+            `${ents[0].id} (${ents.map((e) => e.path.join('.')).join(',')})`,
+        )
+        .join(', ')}`,
+    );
+  }
+  const uniqueEntityIds = uniq(entities.map((e) => e.id));
+  if (uniqueEntityIds.length !== entities.length) {
+    throw new Error(`Found duplicate entity IDs`);
+  }
+}
+
+// Use the extracted function
+validateUniqueEntityIds(entities);
packages/project-builder-web/src/pages/apps/edit/admin/AdminEditSectionForm.tsx (1)

74-98: Consider adding loading feedback.

While the save operation is in progress, consider showing a loading indicator or progress message to improve user experience during potentially long operations.

 return saveDefinitionWithFeedback(
   (config) => {
     const adminApp = config.apps.find((app) => app.id === appConfig.id);
     if (adminApp?.type !== 'admin') {
       throw new Error('Cannot add a section to a non-admin app');
     }

     adminApp.sections = sortBy(
       [
         ...(adminApp.sections ?? []).filter(
           (section) => !sectionId || section.id !== sectionId,
         ),
         { ...data, id },
       ],
       [(section) => section.name],
     );
   },
   {
+    loadingMessage: 'Saving section...',
     onSuccess: () => {
       if (!sectionId) {
         navigate(`edit/${adminSectionEntityType.toUid(id)}`);
       }
     },
   },
 );
packages/project-builder-web/src/pages/settings/theme-builder.tsx (1)

106-114: Clarify or remove the TODO comment.

The TODO comment suggests that this alert might need to be removed, but it's unclear why. Please clarify the conditions under which this alert should be removed or remove it if it's no longer needed.

Would you like me to help track this TODO by creating a new issue?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 76f4247 and 3e24f5a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (107)
  • .changeset/afraid-foxes-build.md (1 hunks)
  • packages/project-builder-lib/src/compiler/app-compiler-spec.ts (1 hunks)
  • packages/project-builder-lib/src/definition/project-definition-container.ts (5 hunks)
  • packages/project-builder-lib/src/index.ts (0 hunks)
  • packages/project-builder-lib/src/migrations/index.ts (2 hunks)
  • packages/project-builder-lib/src/parser/index.ts (1 hunks)
  • packages/project-builder-lib/src/parser/parser.ts (2 hunks)
  • packages/project-builder-lib/src/parser/plugins/auth0.ts (0 hunks)
  • packages/project-builder-lib/src/parser/types.ts (0 hunks)
  • packages/project-builder-lib/src/plugins/schema/parser.ts (1 hunks)
  • packages/project-builder-lib/src/references/deserialize-schema.ts (2 hunks)
  • packages/project-builder-lib/src/references/fix-ref-deletions.ts (2 hunks)
  • packages/project-builder-lib/src/references/parse-schema-with-references.ts (1 hunks)
  • packages/project-builder-lib/src/references/ref-builder.ts (20 hunks)
  • packages/project-builder-lib/src/references/ref-builder.unit.test.ts (1 hunks)
  • packages/project-builder-lib/src/references/resolve-zod-ref-payload-names.ts (1 hunks)
  • packages/project-builder-lib/src/references/resolve-zod-ref-payload-names.unit.test.ts (1 hunks)
  • packages/project-builder-lib/src/references/serialize-schema.ts (2 hunks)
  • packages/project-builder-lib/src/references/types.ts (4 hunks)
  • packages/project-builder-lib/src/schema/models/index.ts (1 hunks)
  • packages/project-builder-lib/src/schema/models/transformers/built-in-transformers.ts (3 hunks)
  • packages/project-builder-lib/src/tools/model-merger/index.ts (1 hunks)
  • packages/project-builder-lib/src/tools/model-merger/model-merger.ts (1 hunks)
  • packages/project-builder-lib/src/web/hooks/useBlockUnsavedChangesNavigate.ts (2 hunks)
  • packages/project-builder-lib/src/web/hooks/useProjectDefinition.ts (1 hunks)
  • packages/project-builder-lib/src/web/hooks/useResettableForm.ts (3 hunks)
  • packages/project-builder-server/src/api/context.ts (1 hunks)
  • packages/project-builder-server/src/api/index.ts (1 hunks)
  • packages/project-builder-server/src/api/plugins.ts (1 hunks)
  • packages/project-builder-server/src/api/projects.ts (1 hunks)
  • packages/project-builder-server/src/api/sync.ts (1 hunks)
  • packages/project-builder-server/src/api/version.ts (1 hunks)
  • packages/project-builder-server/src/compiler/admin/crud/displays.ts (3 hunks)
  • packages/project-builder-server/src/compiler/app-entry-builder.ts (0 hunks)
  • packages/project-builder-server/src/compiler/backend/enums.ts (2 hunks)
  • packages/project-builder-server/src/compiler/backend/fastify.ts (1 hunks)
  • packages/project-builder-server/src/compiler/backend/feature.ts (2 hunks)
  • packages/project-builder-server/src/compiler/backend/services.ts (3 hunks)
  • packages/project-builder-server/src/runner/index.ts (1 hunks)
  • packages/project-builder-server/src/server/plugin.ts (4 hunks)
  • packages/project-builder-server/src/service/builder-service.ts (4 hunks)
  • packages/project-builder-server/src/utils/typed-event-emitter.ts (0 hunks)
  • packages/project-builder-web/package.json (2 hunks)
  • packages/project-builder-web/public/preview-app.json (15 hunks)
  • packages/project-builder-web/src/app/App.tsx (2 hunks)
  • packages/project-builder-web/src/app/AppLayout/AppLayout.tsx (1 hunks)
  • packages/project-builder-web/src/app/AppLayout/ProjectSyncModal.tsx (3 hunks)
  • packages/project-builder-web/src/app/ClientVersionProvider/ClientVersionProvider.test.tsx (1 hunks)
  • packages/project-builder-web/src/app/ClientVersionProvider/ClientVersionProvider.tsx (2 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/NewProjectCard.tsx (4 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/ProjectDefinitionProvider.tsx (1 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-project-definition-container.ts (1 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-remote-project-definition-contents.ts (1 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-schema-parser-context.ts (1 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/services/parse-project-definition-contents.ts (1 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/services/project-definition-file-manager.ts (1 hunks)
  • packages/project-builder-web/src/app/ProjectDefinitionProvider/services/schema-parser-context-manager.ts (1 hunks)
  • packages/project-builder-web/src/app/ProjectSelectorGate/ProjectSelectorDialog.tsx (2 hunks)
  • packages/project-builder-web/src/app/ProjectSelectorGate/ProjectSelectorGate.tsx (2 hunks)
  • packages/project-builder-web/src/app/components/ProjectDefinitionGate.tsx (0 hunks)
  • packages/project-builder-web/src/components/Console/index.tsx (2 hunks)
  • packages/project-builder-web/src/components/FormActionBar/index.tsx (3 hunks)
  • packages/project-builder-web/src/hooks/useClientVersion.ts (0 hunks)
  • packages/project-builder-web/src/hooks/useProjects.ts (1 hunks)
  • packages/project-builder-web/src/hooks/useRemoteProjectDefinition.ts (0 hunks)
  • packages/project-builder-web/src/pages/apps/AppsLayout.page.tsx (1 hunks)
  • packages/project-builder-web/src/pages/apps/edit.page.tsx (3 hunks)
  • packages/project-builder-web/src/pages/apps/edit/BackendAppForm.tsx (2 hunks)
  • packages/project-builder-web/src/pages/apps/edit/WebAppForm.tsx (2 hunks)
  • packages/project-builder-web/src/pages/apps/edit/admin/AdminEditSectionForm.tsx (7 hunks)
  • packages/project-builder-web/src/pages/apps/edit/admin/AdminGeneralForm.tsx (2 hunks)
  • packages/project-builder-web/src/pages/apps/edit/admin/crud/AdminCrudEmbeddedForm.tsx (1 hunks)
  • packages/project-builder-web/src/pages/apps/edit/admin/crud/AdminCrudSectionForm.tsx (1 hunks)
  • packages/project-builder-web/src/pages/apps/edit/admin/crud/CrudTableColumnsForm.tsx (2 hunks)
  • packages/project-builder-web/src/pages/apps/list.page.tsx (1 hunks)
  • packages/project-builder-web/src/pages/apps/new.page.tsx (3 hunks)
  • packages/project-builder-web/src/pages/data/enums/edit/components/EnumHeaderBar.tsx (3 hunks)
  • packages/project-builder-web/src/pages/data/enums/edit/components/EnumInfoEditDialog.tsx (2 hunks)
  • packages/project-builder-web/src/pages/data/enums/edit/edit.page.tsx (2 hunks)
  • packages/project-builder-web/src/pages/data/enums/hooks/useEnumForm.tsx (5 hunks)
  • packages/project-builder-web/src/pages/data/enums/new/NewEnumDialog.tsx (2 hunks)
  • packages/project-builder-web/src/pages/data/models/_hooks/useEditedModelConfig.tsx (1 hunks)
  • packages/project-builder-web/src/pages/data/models/_hooks/useModelForm.ts (4 hunks)
  • packages/project-builder-web/src/pages/data/models/edit/[id]/_components/ModelHeaderBar.tsx (3 hunks)
  • packages/project-builder-web/src/pages/data/models/edit/[id]/_components/fields/ModelFieldTypeInput.tsx (1 hunks)
  • packages/project-builder-web/src/pages/data/models/edit/[id]/graphql.page.tsx (1 hunks)
  • packages/project-builder-web/src/pages/data/models/edit/[id]/index.page.tsx (1 hunks)
  • packages/project-builder-web/src/pages/data/models/edit/[id]/service.page.tsx (1 hunks)
  • packages/project-builder-web/src/pages/features/auth/auth-models.ts (1 hunks)
  • packages/project-builder-web/src/pages/features/auth/index.tsx (4 hunks)
  • packages/project-builder-web/src/pages/plugins/PluginCard.tsx (5 hunks)
  • packages/project-builder-web/src/pages/plugins/home.page.tsx (3 hunks)
  • packages/project-builder-web/src/pages/plugins/plugin-config.page.tsx (3 hunks)
  • packages/project-builder-web/src/pages/settings/components/FeatureForm.tsx (6 hunks)
  • packages/project-builder-web/src/pages/settings/hierarchy.tsx (2 hunks)
  • packages/project-builder-web/src/pages/settings/project-settings.tsx (2 hunks)
  • packages/project-builder-web/src/pages/settings/theme-builder.tsx (4 hunks)
  • packages/project-builder-web/src/services/api/errors.ts (1 hunks)
  • packages/project-builder-web/src/services/api/index.ts (1 hunks)
  • packages/project-builder-web/src/services/api/plugins.ts (1 hunks)
  • packages/project-builder-web/src/services/api/project-definition.ts (1 hunks)
  • packages/project-builder-web/src/services/api/projects.ts (1 hunks)
  • packages/project-builder-web/src/services/api/sync.ts (1 hunks)
  • packages/project-builder-web/src/services/api/version.ts (1 hunks)
  • packages/project-builder-web/src/services/config.ts (1 hunks)
  • packages/project-builder-web/src/services/error-formatter.ts (1 hunks)
  • packages/project-builder-web/src/services/hot-loader.ts (1 hunks)
⛔ Files not processed due to max files limit (23)
  • packages/project-builder-web/src/services/remote.ts
  • packages/project-builder-web/src/services/trpc.ts
  • packages/project-builder-web/src/tests/setup.ts
  • packages/project-builder-web/src/utils/typed-event-emitter.ts
  • packages/project-builder-web/vitest.config.ts
  • packages/sync/src/utils/evented-logger.ts
  • packages/sync/src/utils/index.ts
  • packages/sync/src/utils/typed-event-emitter.ts
  • packages/tools/eslint-configs/react.js
  • packages/tools/eslint-configs/tailwind.js
  • packages/ui-components/package.json
  • packages/ui-components/src/components/ErrorDisplay/ErrorDisplay.tsx
  • packages/ui-components/src/components/Loader/Loader.tsx
  • packages/ui-components/src/components/Toaster/Toaster.tsx
  • packages/utils/src/crypto/hash.ts
  • packages/utils/src/crypto/index.ts
  • packages/utils/src/events/index.ts
  • packages/utils/src/events/typed-event-emitter.ts
  • packages/utils/src/events/typed-event-emitter.unit.test.ts
  • packages/utils/src/index.ts
  • plugins/baseplate-plugin-storage/src/storage/core/components/CategoryEditorForm.tsx
  • plugins/baseplate-plugin-storage/src/storage/core/components/StorageConfig.tsx
  • plugins/baseplate-plugin-storage/src/storage/transformers/types.ts
💤 Files with no reviewable changes (8)
  • packages/project-builder-lib/src/index.ts
  • packages/project-builder-server/src/compiler/app-entry-builder.ts
  • packages/project-builder-web/src/app/components/ProjectDefinitionGate.tsx
  • packages/project-builder-lib/src/parser/plugins/auth0.ts
  • packages/project-builder-server/src/utils/typed-event-emitter.ts
  • packages/project-builder-lib/src/parser/types.ts
  • packages/project-builder-web/src/hooks/useClientVersion.ts
  • packages/project-builder-web/src/hooks/useRemoteProjectDefinition.ts
✅ Files skipped from review due to trivial changes (5)
  • .changeset/afraid-foxes-build.md
  • packages/project-builder-lib/src/parser/index.ts
  • packages/project-builder-web/src/app/AppLayout/AppLayout.tsx
  • packages/project-builder-web/src/services/api/index.ts
  • packages/project-builder-lib/src/tools/model-merger/index.ts
🔇 Additional comments (209)
packages/project-builder-web/src/pages/settings/project-settings.tsx (2)

27-35: Great optimization using useMemo for form values!

The memoization of defaultValues prevents unnecessary recalculations when the component re-renders, improving performance. The values are properly derived from the definition using the pick utility.


39-43: Clean implementation of the new save flow!

The new submission logic using saveDefinitionWithFeedback aligns perfectly with the PR's objective to implement asynchronous saving capabilities. The use of Object.assign for updating the draft config is clean and efficient.

packages/project-builder-web/src/pages/data/models/_hooks/useEditedModelConfig.tsx (1)

74-76: LGTM! Error message now accurately reflects the required context.

The updated error message provides clearer guidance by correctly referencing the EditedModelContextProvider, improving the developer experience.

packages/project-builder-web/src/pages/plugins/plugin-config.page.tsx (3)

18-24: LGTM! Good improvements to state management.

The addition of saveDefinitionWithFeedbackSync and isSavingDefinition enhances the user experience by providing better feedback during save operations.


107-109: LGTM! Good UX improvement.

The use of isSavingDefinition to disable the button during save operations prevents potential race conditions from multiple submissions.


88-92: Verify the implementation of onSave.

The onSave function only navigates away if there's no plugin definition but doesn't perform any actual save operation. This seems incomplete or incorrect given the function name and its usage in the Container component.

Please verify:

  1. Should this function actually save something?
  2. Is this a leftover from previous code?
  3. Is the save functionality handled within the Container component instead?
packages/project-builder-lib/src/web/hooks/useBlockUnsavedChangesNavigate.ts (3)

2-2: LGTM! Import changes align with the new form state management approach.

The addition of Control type and useFormState hook from react-hook-form supports the refactored form state management.


7-17: LGTM! Function signature changes follow React Hook Form best practices.

The refactor to use control instead of formState is a good improvement as it:

  • Follows React Hook Form's recommended patterns
  • Enables internal form state management
  • Maintains type safety through generics

18-20: LGTM! Form state management is now better encapsulated.

The internal derivation of form state using useFormState hook improves the component's encapsulation while maintaining the same functionality.

packages/project-builder-web/src/pages/apps/edit/admin/AdminGeneralForm.tsx (2)

25-30: LGTM! Improved state management with better feedback mechanisms.

The updated useProjectDefinition hook destructuring provides clearer access to the project definition state and saving functionality, aligning well with the PR's objectives of improving the sync/save flow.


36-40: LGTM! More controlled form initialization.

The switch from defaultValues to values in useResettableForm provides better control over form initialization, ensuring the form always reflects the current appConfig.

packages/project-builder-web/src/pages/settings/hierarchy.tsx (1)

24-25: LGTM! Good use of the new save flow.

The change to use saveDefinitionWithFeedbackSync aligns well with the PR's objective of improving the synchronization and saving process.

packages/project-builder-web/src/pages/apps/edit/admin/crud/AdminCrudSectionForm.tsx (1)

31-31:

Details

✅ Verification successful

LGTM! Verify consistent usage across components.

The change to use definition directly from useProjectDefinition aligns with the broader refactor of project definition handling.

Let's verify that this pattern is consistently applied across other components:


🏁 Script executed:

#!/bin/bash
# Description: Check for consistent usage of `definition` across components.

# Test: Search for any remaining instances of `parsedProject`. Expect: No occurrences.
rg -A 5 $'parsedProject.*=.*useProjectDefinition'

# Test: Search for the new pattern. Expect: Multiple occurrences showing consistent usage.
rg -A 5 $'definition.*=.*useProjectDefinition'

Length of output: 26265


LGTM – Consistent usage confirmed.

The change in AdminCrudSectionForm.tsx to extract definition directly from useProjectDefinition() is consistent with how other components now access project definitions. The executed script confirms that outdated parsedProject references have been fully removed and that the new pattern (definition or definitionContainer, as contextually required) is uniformly applied across the codebase.

packages/project-builder-web/src/pages/data/models/_hooks/useModelForm.ts (2)

23-23: LGTM! Improved error handling with dedicated error types.

The changes enhance error handling by using a dedicated NotFoundError type and a unified saveDefinitionWithFeedback mechanism.

Also applies to: 64-64


166-196: LGTM! Well-structured definition saving with proper feedback.

The implementation provides a clear and consistent user experience with:

  • Operation-specific success messages
  • Proper form state management
  • Appropriate navigation after success
packages/project-builder-web/src/components/FormActionBar/index.tsx (3)

7-12: LGTM! Well-structured interface extension.

The addition of the optional disabled prop enhances the component's flexibility for handling asynchronous operations.


14-17: LGTM! Clean component signature update.

The destructuring pattern is well-implemented and maintains type safety.


30-30:

Details

✅ Verification successful

Consider extracting the disabled logic to reduce duplication.

The disabled state logic is duplicated between both buttons. Consider extracting it to a constant or memoized value.

 const FormActionBar = ({
   form,
   disabled,
 }: FormActionBarProps): React.JSX.Element => {
   const { formState } = form;
   const isDirty = Object.keys(formState.dirtyFields).length > 0;
+  const isDisabled = formState.isSubmitting || !isDirty || disabled;

   return (
     <div className="absolute inset-x-0 bottom-0 z-50 flex min-h-[--action-bar-height] items-center space-x-4 border-t border-border bg-white pl-4">
       <Button
         variant="outline"
         size="sm"
         type="button"
         onClick={() => {
           form.reset();
         }}
-        disabled={formState.isSubmitting || !isDirty || disabled}
+        disabled={isDisabled}
       >
         Reset
       </Button>
       <Button.WithIcon
         icon={MdOutlineSave}
         variant="default"
         size="sm"
         type="submit"
-        disabled={formState.isSubmitting || !isDirty || disabled}
+        disabled={isDisabled}
       >
         Save
       </Button.WithIcon>
     </div>
   );
 };

Let's verify the disabled state handling is consistent with other form components:

Also applies to: 39-39


🏁 Script executed:

#!/bin/bash
# Search for other form components using FormActionBar to verify disabled prop usage
rg -A 5 "FormActionBar" packages/project-builder-web/src/

Length of output: 9901


Refactor: Extract duplicated disabled logic in FormActionBar

The current implementation repeats the same disabled state computation for both the Reset and Save buttons. To improve clarity and reduce duplication, consider extracting this logic into a dedicated constant. This approach has been verified across the codebase and is consistent with how FormActionBar is used in other components.

  • Location: packages/project-builder-web/src/components/FormActionBar/index.tsx (around lines 30).
  • Action: Define a constant (e.g., isDisabled) with the logic formState.isSubmitting || !isDirty || disabled and use it for both buttons.
 const FormActionBar = ({
   form,
   disabled,
 }: FormActionBarProps): React.JSX.Element => {
   const { formState } = form;
   const isDirty = Object.keys(formState.dirtyFields).length > 0;
+  const isDisabled = formState.isSubmitting || !isDirty || disabled;

   return (
     <div className="absolute inset-x-0 bottom-0 z-50 flex min-h-[--action-bar-height] items-center space-x-4 border-t border-border bg-white pl-4">
       <Button
         variant="outline"
         size="sm"
         type="button"
         onClick={() => {
           form.reset();
         }}
-        disabled={formState.isSubmitting || !isDirty || disabled}
+        disabled={isDisabled}
       >
         Reset
       </Button>
       <Button.WithIcon
         icon={MdOutlineSave}
         variant="default"
         size="sm"
         type="submit"
-        disabled={formState.isSubmitting || !isDirty || disabled}
+        disabled={isDisabled}
       >
         Save
       </Button.WithIcon>
     </div>
   );
 };
packages/project-builder-web/src/pages/apps/AppsLayout.page.tsx (2)

16-16: LGTM! Simplified data access pattern.

The change to access apps directly from definition improves code readability while maintaining type safety.


14-14:

Details

✅ Verification successful

Verify hook usage across the codebase.

The change from parsedProject to definition is part of the broader refactoring effort. Let's verify that all components using this hook have been updated consistently.


🏁 Script executed:

#!/bin/bash
# Search for any remaining usage of parsedProject from useProjectDefinition
ast-grep --pattern 'const { parsedProject } = useProjectDefinition()'

Length of output: 71


Below is a new shell script to further verify that no lingering references to the old property parsedProject remain in the repository. Please run the script and share the output (if any) so we can confirm that the refactor has been applied consistently across the codebase.

Once confirmed that no results are returned, we can conclude the hook usage has been updated properly.


🏁 Script executed:

[run_scripts]
#!/bin/bash
echo "Searching for any lingering references to 'parsedProject' in the repository..."
rg "parsedProject" -n .

Length of output: 318


Consistent Hook Update – No Latent Issues Found

The change from extracting parsedProject to definition from the useProjectDefinition hook is aligned with the refactoring objectives. Our verification (via the searches for "parsedProject") did not reveal any lingering references across the codebase, suggesting that the update has been applied consistently.

  • Confirmed that no files still reference parsedProject.
  • The rename supports the intended refactor for the sync/save flow.
packages/project-builder-web/src/pages/data/enums/edit/edit.page.tsx (3)

20-20: LGTM! Clean refactor of hook parameters.

The refactored useBlockUnsavedChangesNavigate call using object parameters improves code readability and maintainability.


32-32: LGTM! Enhanced UX with disabled state.

The addition of the disabled prop to FormActionBar prevents multiple submissions while saving, improving user experience.


15-15:

Details

❓ Verification inconclusive

LGTM! Verify formState usage.

The addition of isSavingDefinition state improves UX by tracking form submission state. The removal of formState from destructuring suggests a change in form state management.

Also applies to: 18-18


🏁 Script executed:

#!/bin/bash
# Description: Check if formState is used elsewhere in the codebase
rg -A 5 'formState' packages/project-builder-web/src/pages/data/enums

Length of output: 69


I'll perform an additional check to see if any file within packages/project-builder-web still references formState. Please run the following shell script to search the entire project for any occurrences of "formState":

Once you verify that no results are returned (or that any identified usages are intentional and do not impact the updated state management), you can conclude that the removal of formState from the destructuring in favor of the new isSavingDefinition state is correct.


LGTM! Verified that the removal of formState is consistent with the new usage of isSavingDefinition to track form submission state. Please ensure manually that no dependent functionality relies on formState in the project.


🏁 Script executed:

#!/bin/bash
# Checking for any usage of 'formState' in the entire project-builder-web directory
rg -n "formState" packages/project-builder-web

Length of output: 925


LGTM!

  • The change in packages/project-builder-web/src/pages/data/enums/edit/edit.page.tsx to replace formState with the new isSavingDefinition state for tracking form submission is intentional.
  • Our search confirms that while formState is still actively used in other parts of the project (e.g., in NewProjectCard, ServiceTransformerForm, FeatureForm, and within FormActionBar), its removal in this enum editing page appears isolated.
  • Please ensure that no dependencies in this context expect formState and verify that the updated state management meets the intended UX requirements.
packages/project-builder-web/src/pages/data/enums/edit/components/EnumInfoEditDialog.tsx (2)

31-31: LGTM! Added form submission state.

The addition of isSavingDefinition state enables better control over form submission.


59-61: LGTM! Enhanced UX with disabled state.

The disabled state on the submit button prevents multiple submissions and provides visual feedback during save operation.

packages/project-builder-web/src/pages/data/enums/new/NewEnumDialog.tsx (2)

29-29: LGTM! Added form submission state.

The addition of isSavingDefinition state enables better control over form submission.


74-76: LGTM! Enhanced UX with disabled state.

The disabled state on the create button prevents multiple submissions and provides visual feedback during save operation.

packages/project-builder-server/src/api/context.ts (2)

3-5: Imports look good.

These imports correctly bring in TRPCError for error handling and the BaseplateApiContext type without any apparent issues.


27-29: Context type definition appears correct.

Defining the Context type as the awaited return of the function ensures proper typing. No further issues found.

packages/project-builder-server/src/api/projects.ts (4)

4-4: Import of ProjectDefinitionFilePayload is correct.

This import aligns with your usage in the subscription for 'project-json-changed' events.


41-51: Read definition logic looks straightforward.

Fetching the service through ctx.getApi(id) and returning contents is clear and matches the code’s intent.


53-66: Subscription approach is appropriate.

Using an observable to emit updates when 'project-json-changed' triggers is a clean solution. No issues found.


68-80: Write definition mutation is properly structured.

Calling api.writeDefinition with the new contents and old hash is consistent with typical optimistic concurrency checks. No issues found.

packages/project-builder-lib/src/migrations/index.ts (2)

28-40: Custom error class is well-structured.

Including the migration name and the underlying cause is helpful for debugging and logging. No further issues found.


43-65: Renaming to "migratedDefinition" clarifies intent.

Renaming newConfig to migratedDefinition improves readability. The try/catch around each migration call and the custom SchemaMigrationError facilitate more robust error handling.

packages/project-builder-web/src/app/ClientVersionProvider/ClientVersionProvider.tsx (4)

5-12: Imports look good.

These imports from various internal services, hooks, and error logging are well-aligned with their usage in the subsequent code. Good job keeping everything in one place.


14-17: Interface definition is concise and clear.

Defining just a children prop is sufficient for this provider. This approach keeps the component flexible while clearly communicating usage.


18-20: Provider function signature is well-defined.

Renaming to ClientVersionProvider and accepting the new props matches your new approach for version management.


37-43: WebSocket reconnection logic looks good.

Attaching the fetchVersion call to the 'open' event ensures the version is refreshed automatically after re-establishing the connection, and returning the unsubscribe callback properly cleans up the listener.

packages/project-builder-web/src/pages/data/models/edit/[id]/_components/ModelHeaderBar.tsx (4)

6-6: Revised imports help maintain consistency with UI patterns.

Using Button and useConfirmDialog from the same UI suite keeps interactions consistent across your application.


22-23: Destructuring from useProjectDefinition is clear.

Extracting definition, saveDefinitionWithFeedbackSync, and isSavingDefinition clarifies which functionalities your component depends on.


28-37: Deletion flow is streamlined.

Shifting to saveDefinitionWithFeedbackSync with an inline draft updater and success callback simplifies error handling and ensures consistent feedback. This is a clean, unified approach.


69-69: Disabling the delete button during saves is a good UX practice.

Preventing multiple concurrent actions helps avoid race conditions or partial updates.

packages/project-builder-web/src/app/ProjectDefinitionProvider/services/project-definition-file-manager.ts (7)

1-2: Utility import usage is appropriate.

Importing hashWithSHA256 from a shared utility is straightforward and helps ensure a consistent hashing approach across the codebase.


3-7: API imports align with manager responsibilities.

Downloading, uploading, and listening for definition file changes belong logically in this manager, keeping network concerns encapsulated.


9-13: Interface definition is straightforward.

Exposing contents, hash, and updatedExternally is sufficient for client usage. Good job limiting the interface to essentials.


15-23: Class design is modular and clear.

Encapsulating file logic in ProjectDefinitionFileManager fosters separation of concerns. The projectId is well-scoped to an instance.


25-41: Download logic is concise with correct hashing checks.

Your updatedExternally condition helps detect external modifications reliably. Consider elaborating concurrency strategies if external writes can happen during multiple downloads.


42-87: Upload logic handles concurrency well.

Throwing an error if there's already a pending upload clarifies usage. The final block resets _pendingDefinitionFileContentsHash for re-entrancy. This is a solid approach to avoid overwriting in-flight changes.


89-108: Change listener updates local hash and calls back.

This method effectively detects remote updates while respecting the local _definitionFileContentsHash. Great pattern for pushing updates to the consumer.

packages/project-builder-web/src/pages/apps/edit.page.tsx (5)

6-6: No concerns on this import update.
The addition of Button and Dialog references looks straightforward and aligns with the UI changes below.


16-16: Destructure usage updated successfully.
Swapping out legacy references in favor of saveDefinitionWithFeedbackSync, definition, and isSavingDefinition is a clean approach to unify the control flow. Ensure all previous references to parsedProject and setConfigAndFixReferences are removed from dependent components.


28-40: Enhanced delete handling logic is well-structured.

  1. Using saveDefinitionWithFeedbackSync with a callback filtering the deleted app from definition.apps is both succinct and clear.
  2. The options object providing successMessage and an onSuccess callback simplifies post-deletion flow.

Overall, this block is neatly handling user feedback and navigation. Just ensure any references to the removed app are also pruned if necessary in upstream code.


43-43: Accessing packageScope from definition
This is consistent with the new state shape. Make sure other references to definition's fields are updated consistently to avoid undefined or stale state.


74-78: Disable button during save is a user-friendly improvement.
Disabling the destructive button when isSavingDefinition is true prevents duplicate deletions and race conditions.

packages/project-builder-lib/src/references/fix-ref-deletions.ts (6)

6-10: New type imports are aligned with the updated reference-handling approach.
Bringing in ResolvedZodRefPayload consolidates type safety and clarity in the reference flow.


12-12: Importing parseSchemaWithReferences.
This is a logical and well-named function to parse the schema thoroughly. Make sure that all references to legacy parse calls are removed if not needed.


17-17: Returning refPayload in success results.
Including the parsed reference payload in the result object improves post-processing, especially when further reference manipulations might be needed. This is an excellent improvement in the function's output structure.


46-49: Initializing iteration count and valueToEdit.

  1. The for loop approach is valid to mitigate infinite recursion when references are scattered.
  2. Consider if an exponential backoff or bail-out condition might be needed beyond the iteration cap to detect cyclical references.

73-93: Handling missing array references via tryDeleteParent.

  1. Checking if the parent is an array and marking entries for deletion is a neat approach.
  2. The code ensures array indexes are tracked in reverse order to avoid index shifting.
  3. Confirm that the sentinel 'deleted-sentinel-id' is never incorrectly assigned in normal usage, so we don’t accidentally skip legitimate references.

95-145: Comprehensive fix flow with repeated parse attempts.

  1. The function gracefully addresses leftover references with each iteration.
  2. Returning a failure result once the iteration ends or issues remain is robust.
  3. The final throw new Error(...) for exceeding max iterations helps avoid infinite loops—good safeguard.
packages/project-builder-web/src/app/ProjectDefinitionProvider/ProjectDefinitionProvider.tsx (16)

1-7: Importing new types for type-safe project definition management.
Utilizing these definitions ensures better clarity when passing around setter and feedback options.


8-13: Importing reference-fixing utilities and context.
Centralizing logic in project-builder-lib fosters consistency. Good usage of typed references to unify schema parsing.


16-20: UI components import block.
Looks correct, with toast providing an easy way to show messages—standard for user feedback.


21-21: Immer-based immutability usage.
The produce function from immer is a clean approach for atomic state updates.


22-23: Initialization of local states and derived values.
Using useState for isSavingDefinition and useMemo for result is an excellent move for performance. Just ensure that any derived states are invalidated on relevant dependencies.


29-29: useDeleteReferenceDialog
This custom hook presumably surfaces a UI flow for reference deletion issues. It’s consistent with your broader reference deletion logic.


31-38: ProjectDefinitionProviderProps interface
The optional children property ensures flexibility in usage of the provider.


40-58: Provider definition: main logic initialization

  1. The code properly fetches, parses, and sets up the environment for the project definition.
  2. The approach ensures that errors from multiple sources (contextError, definitionError, containerError) are captured.

59-62: updatedExternally state
A helpful signal to detect changes that happen outside this flow. Consider how it’s consumed in the rest of the app to avoid overwriting external modifications.


63-65: useMemo block check
Exiting early if no projectDefinitionContainer or schemaParserContext is found is a good safety measure.


66-105: saveDefinition function

  1. Using immer to prepare a new project definition is clean and predictable.
  2. fixRefDeletions is called to ensure references are valid after mutation.
  3. Properly reassigning cliVersion keeps versioning consistent.
  4. The try/finally ensures loading state resets in all outcomes. Nicely done.

107-136: saveDefinitionWithFeedback

  1. Handling user notifications (success, error, or reference issues) is well abstracted.
  2. The optional disableDeleteRefDialog flag prevents unwanted UI prompts when desired.
  3. Returning a success object after .then() is a simple way to chain calls.

137-148: Exposing saveDefinitionWithFeedbackSync

  1. Wrapping saveDefinitionWithFeedback in a sync-like call is a neat approach for event-based triggers (like button onClicks).
  2. Consider also returning the promise or an indication in case the caller needs further hooking.
    [architectural_advice]

159-205: Error and loading states

  1. The ErrorableLoader usage is straightforward, showing fallback UIs if something fails.
  2. The approach of switching the project is user-friendly.

208-247: CLI version conflict check

  1. Validating cliVersion and semver.gt is a good approach to ensure the user has the right environment.
  2. The fallback explanatory UI with an option to switch projects is a smart user experience design.

249-253: Final provider structure
Wrapping children in ProjectDefinitionContext.Provider ensures broad reusability for everything inside your application.

packages/project-builder-server/src/service/builder-service.ts (7)

19-19: Usage of promises-based file I/O is good.
Switching to readFile/writeFile from 'node:fs/promises' is a clean, modern approach.


42-54: Write result union type is well-structured.
This union type clearly differentiates successful writes from content mismatch errors. As a future enhancement, you might consider adding more error/result states, such as filesystem permission issues.


70-73: Typed event interface helps reduce runtime errors.
Explicitly defining 'project-json-changed' and 'command-console-emitted' events promotes clarity and maintainability.


75-75: Leverage generics in TypedEventEmitter.
Using TypedEventEmitter<ProjectBuilderServiceEvents> ensures event parameters are strongly typed, preventing accidental misusage of event payloads.


128-136: Sensible approach to JSON file changes.
Reading the latest definition and emitting 'project-json-changed' keeps the file watcher reactive and consistent. Error logging is appropriate.


148-148: Watcher initialization is straightforward.
Chokidar is employed effectively here. If performance becomes a concern in the future, consider more granular watchers or debouncing.


171-185: Read method is concise and robust.
Handling a missing file with handleFileNotFoundError and falling back to _getInitialProjectDefinition is a neat approach. Thread-safe concerns appear minimal here, but keep concurrency in mind if you expand behavior.

packages/project-builder-web/src/pages/features/auth/index.tsx (11)

5-5: Great use of model patching utilities in imports.
Introducing applyModelPatchInPlace, diffModel, and ModelUtils helps unify model transformations. This is likely to reduce boilerplate elsewhere.

Also applies to: 9-10


16-16: UiAlert import is beneficial.
Wrapping warnings or notifications in a dedicated alert component can streamline user messaging.


18-18: Cleanly separated usage of React hooks and local components.
Using useState, useMemo, and a custom Button keeps the code modular.

Also applies to: 19-19


23-23: Refactor for dedicated model generation.
createAuth0Models centralizes Auth0-specific model creation, promoting Single Responsibility and maintainability.


27-32: Extend project definition for sync and feedback.
Destructuring definitionContainer, saveDefinitionWithFeedback, and saveDefinitionWithFeedbackSync is a clear approach for flexible save operations.


43-45: Reading model references from definition.
Storing userModelRef and userRoleModelRef helps ensure that the correct models are leveraged for reference resolution.


63-86: onSubmit patch logic is well-organized.
Applying the patches to the draftConfig ensures an atomic update. Good job utilizing pendingModelChanges check.


102-112: Graceful disableAuth flow.
saveDefinitionWithFeedbackSync plus an onSuccess handler is succinct and user-friendly, clearing out local form states. Make sure to log or handle any potential errors.


129-131: Form submission wiring is consistent.
onSubmit references are properly bound, and the disableAuth button is placed logically.


132-166: Detailed user model change alerts.
Presenting the number of fields, relations, and constraints to be updated improves transparency. This is user-friendly.


167-202: Detailed user role model change alerts.
Similar to user model changes, providing breakdown fosters clarity for the user. Make sure to keep the UI consistent if additional role model changes are introduced.

packages/project-builder-lib/src/tools/model-merger/model-merger.ts (12)

1-2: Useful utility import for structural comparison.
isEqual from es-toolkit is a simple, effective way to handle deep comparison of objects.


3-9: Separate definition and schema references.
Importing ProjectDefinitionContainer and various configs cleanly clarifies dependencies. Good practice.


11-19: Helper imports for ID generation and references.
Leveraging the ModelUtils and typed entity references will reduce manual overhead when generating new IDs or referencing models.


22-27: DiffOperation interface is concise.
Providing a clear enum-like 'add' | 'update' | 'remove' with a key makes patch application straightforward.


28-41: Straightforward scalar, relation, and constraint types.
Omitting the id fields helps ensure that new items can be assigned IDs on creation.


43-51: ModelDefinitionInput shape.
Defining an input structure for fields, relations, and constraints fosters a robust pipeline for diffing and patching.


53-61: ModelDiffOutput enumerates all possible changes.
Capturing added/updated/removed fields, relations, constraints, and primary key changes sets a good foundation for consistent merges.


63-66: Optional removals with enableRemove.
This makes the diff function flexible, accommodating scenarios where removal is or is not desired.


76-106: diffByKey function is well-structured.
Iterating over desired items first to handle adds/updates, then optionally removing unneeded items, is intuitive. The optional enableRemove logic is beneficial.


108-150: applyPatchByKey with ID preservation strategy.
Retaining the existing ID upon update is critical for stable references; throwing an error on missing items for update avoids silent failures.


152-163: Constructing a unique key for constraints.
Sorting field refs ensures consistent key generation. This approach prevents random ordering from generating spurious diffs.


233-309: applyModelPatchInPlace uses a well-defined pipeline.
The final reference resolution logic is especially important to ensure local and foreign refs remain valid. This approach is aligned with standard migration patterns.

packages/project-builder-lib/src/references/ref-builder.ts (10)

23-25: Doc comment addition looks good.
The concise description for zRefId helps clarify its intended usage.


46-49: Doc comment is clear and consistent.
No issues found with the explanation of the "resolve name" mechanism.


69-80: Helper function usage.
createDefinitionEntityNameResolver simply returns the passed resolver, which is fine. No issues found.


82-84: Base entity definition changes are well-documented.
The inclusion of getNameResolver adds flexibility. No issues found.

Also applies to: 85-106


113-114: Doc comments for entity/reference interfaces and builder classes.
All added doc comments increase readability. No concerns or corrections needed.

Also applies to: 125-126, 137-139, 151-152, 178-180, 189-190, 191-194, 200-203


209-219: Constructor and property definitions.
The new class fields (references, entitiesWithNameResolver, etc.) are coherent. No issues found with state initialization.

Also applies to: 220-225, 232-232


239-245: Path construction methods and documentation.
The doc comments explain the reference path logic clearly. Implementation appears logically sound.

Also applies to: 256-261, 268-277


301-355: addReference method logic.
Null/undefined check for reference value is a good safeguard. The parent path checks are correct. No issues found.


357-375: addEntity method enhancements.
Using a dynamic name resolver is a great extension for entity management. Ensure you handle the case where a getNameResolver might return an invalid or empty string.

Also applies to: 381-381, 389-389, 392-392, 393-395, 398-398, 414-422, 423-423


433-451: Bulk changes: ZodRef, ZodRefWrapper, and associated utilities.
These extensive refactors appear consistent, with robust doc comments and correct usage of shared contexts. The layering of parse steps and transformations is well-structured.

Also applies to: 469-471, 474-474, 485-488, 491-491, 501-502, 533-606, 610-617, 619-665, 667-681, 683-748, 751-759, 761-772, 773-846

packages/project-builder-web/src/services/config.ts (1)

12-12: New IS_PREVIEW flag.
Exporting IS_PREVIEW from config is a straightforward approach. No issues found.

packages/project-builder-web/src/services/api/version.ts (1)

1-14: Preview-mode version handling.
Returning a hardcoded preview response vs. querying trpc.version.get.query() aligns well with your preview toggle. Implementation looks consistent.

packages/project-builder-server/src/api/plugins.ts (1)

5-17: LGTM! Clean router implementation.

The router definition is well-structured, with proper input validation and clear separation of concerns. The mutation handler is concise and follows the single responsibility principle.

packages/project-builder-server/src/api/version.ts (2)

5-8: LGTM! Well-defined interface.

The ClientVersionInfo interface is clear and follows TypeScript best practices.


10-17: LGTM! Clean and type-safe router implementation.

The router is well-implemented with proper type annotations and follows tRPC patterns.

packages/project-builder-web/src/services/api/projects.ts (1)

6-22: LGTM! Clean implementation with proper preview mode handling.

The function is well-documented and handles both preview and non-preview modes appropriately, with type-safe mock data.

packages/project-builder-server/src/api/index.ts (1)

2-19: LGTM! Clean router configuration.

The router configuration is well-organized, with clear separation of concerns across different routers. The type exports are properly defined, making the API structure clear and maintainable.

packages/project-builder-web/src/services/error-formatter.ts (1)

9-9: LGTM! More actionable error message.

The new error message provides better guidance by directing users to check console logs for detailed information, which is more helpful for debugging than the previous generic message.

packages/project-builder-web/src/services/hot-loader.ts (1)

1-23: LGTM! Well-implemented hot reload subscription.

The implementation is robust with:

  • Type-safe event handling using generics
  • Proper cleanup through unsubscribe function
  • Graceful handling of environments without hot reload support
  • Clear documentation
packages/project-builder-web/src/services/api/plugins.ts (1)

1-22: LGTM! Well-structured plugin metadata retrieval.

The implementation is solid with:

  • Clear handling of preview mode
  • Proper error handling for project not found cases
  • Well-documented API with JSDoc
  • Type-safe mutation call
packages/project-builder-server/src/compiler/backend/enums.ts (3)

3-3: LGTM!

The import of ProjectDefinition type aligns with the PR's objective of refactoring project definition handling.


20-20: LGTM!

The parameter type change from ParsedProjectDefinition to ProjectDefinition is consistent with the refactoring objectives.


22-24: LGTM!

The changes improve null safety by using the nullish coalescing operator while maintaining the same filtering logic.

packages/project-builder-lib/src/references/parse-schema-with-references.ts (1)

1-9: LGTM!

The imports are well-organized and include all necessary types and functions.

packages/project-builder-server/src/api/sync.ts (1)

8-14: LGTM!

The router definition and input schema are well-structured and type-safe.

packages/project-builder-web/src/app/ProjectSelectorGate/ProjectSelectorGate.tsx (3)

8-8: LGTM! Import path updated.

The import path for getProjects has been updated from src/services/remote to @src/services/api, which aligns with the broader refactoring of the project's service layer.


11-11: LGTM! Component and interface renamed for better clarity.

The renaming from ProjectChooser to ProjectSelector in both the component and interface names provides better clarity about the component's purpose and maintains consistency across the codebase.

Also applies to: 13-15, 20-22


17-19: LGTM! Added descriptive JSDoc comment.

The added JSDoc comment clearly describes the component's purpose: "Gate that ensures a project is selected before rendering the app."

packages/project-builder-web/src/pages/features/auth/auth-models.ts (1)

6-21: LGTM! Well-defined Auth0 user model fields.

The AUTH0_MODEL_FIELDS array provides a clear definition of user model fields with appropriate types and options:

  • UUID generation for the id field
  • Required email field
  • Optional auth0Id field for external authentication
packages/project-builder-lib/src/references/serialize-schema.ts (2)

10-12: LGTM! Enhanced type safety with ResolvedZodRefPayload.

The function signature now uses ResolvedZodRefPayload instead of ZodRefPayload, which better represents the resolved state of references in the payload.


43-43: LGTM! Improved schema parsing mechanism.

Replaced the old ZodRefWrapper.create(schema).parse(value) with the new parseSchemaWithReferences(schema, value) function, which provides a more direct and maintainable approach to parsing schemas with references.

packages/project-builder-lib/src/web/hooks/useResettableForm.ts (2)

19-19: LGTM! Simplified external change tracking.

Replaced the numeric counter with a boolean flag updatedExternally, which provides a clearer and more maintainable way to track external changes.

Also applies to: 31-31


43-43: LGTM! Updated effect dependencies.

The effect's dependency array has been correctly updated to include updatedExternally and isDirty, ensuring proper reactivity to external changes and form state.

packages/project-builder-web/src/hooks/useProjects.ts (1)

1-1: LGTM! Type updates improve consistency.

The change from local Project type to server-side ProjectInfo type aligns with the refactoring objectives and improves type consistency across packages.

Also applies to: 8-8, 10-10

packages/project-builder-web/src/pages/data/models/edit/[id]/graphql.page.tsx (1)

20-22: LGTM! Improved code clarity.

Destructuring form properties and passing specific values to useBlockUnsavedChangesNavigate makes the dependencies clearer and improves code maintainability.

packages/project-builder-web/src/app/ProjectDefinitionProvider/services/parse-project-definition-contents.ts (1)

13-49: LGTM! Well-structured error handling.

The function implements a robust parsing flow with proper error handling for various failure cases. The separation of concerns between parsing, migration, and validation is clear and maintainable.

packages/project-builder-web/src/app/ProjectDefinitionProvider/services/schema-parser-context-manager.ts (1)

11-49: LGTM! Well-designed context management.

The SchemaParserContextManager class provides a clean and maintainable way to handle schema parser context with proper lifecycle management and hot reload support.

packages/project-builder-lib/src/plugins/schema/parser.ts (1)

53-55: LGTM! Clean implementation of the pluginStore getter.

The getter provides a clean and type-safe way to access the pluginStore property while maintaining encapsulation.

packages/project-builder-web/src/app/App.tsx (1)

15-17: LGTM! Well-structured component hierarchy with improved state management.

The refactoring from gate components to providers enhances the state management approach while maintaining a clear separation of concerns. The component tree is well-organized with:

  1. Error handling at the top level
  2. Version management through ClientVersionProvider
  3. Project selection through ProjectSelectorGate
  4. Project definition management through ProjectDefinitionProvider
  5. UI components (Toaster, ConfirmDialog, BlockerDialog) properly placed as direct children of Tooltip.Provider

Also applies to: 32-43

packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-schema-parser-context.ts (2)

11-14: LGTM! Well-defined hook interface with proper state management.

The hook's interface and state management follow React best practices:

  • Clear interface definition with UseSchemaParserContextResult
  • Proper state initialization for both context and error

Also applies to: 21-27


28-54: LGTM! Comprehensive effect handling with proper cleanup.

The effect implementation is robust:

  • Proper cleanup of state on dependency changes
  • Early return for invalid conditions
  • Error handling for both initial load and plugin asset changes
  • Cleanup function returns the listener unsubscribe
packages/project-builder-web/src/pages/apps/list.page.tsx (1)

11-13: LGTM! Simplified data access pattern.

The changes streamline the data access by directly using the definition object, which is consistent with the broader refactoring effort in the codebase.

packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-project-definition-container.ts (1)

11-19: LGTM! Well-structured interface definitions.

The interfaces are clear, properly typed, and follow TypeScript best practices by explicitly marking optional values using undefined union types.

packages/project-builder-web/src/services/api/project-definition.ts (2)

43-60: LGTM! Robust content hash verification.

The hash verification prevents concurrent modification conflicts, enhancing data integrity.


18-33:

Details

❓ Verification inconclusive

Verify preview mode security implications.

The preview mode fetches from a static JSON file. Ensure this doesn't expose sensitive data in production builds.


🏁 Script executed:

#!/bin/bash
# Check if preview-app.json exists in production builds
find . -name "preview-app.json" -type f -exec grep -l "sensitive" {} \;

Length of output: 72


Action: Manually verify preview mode file safety

The function correctly fetches the static JSON file when IS_PREVIEW is enabled. However, our initial grep check (searching for the literal word "sensitive") did not yield any output. This result is inconclusive regarding whether the file might contain other sensitive data. Please manually verify that the preview-app.json content—especially in production builds—is appropriate and does not inadvertently expose any sensitive information.

  • Confirm that preview-app.json is only deployed with preview builds.
  • Double-check the file’s contents for any sensitive data that might not be caught by a simple keyword search.
packages/project-builder-server/src/compiler/backend/feature.ts (1)

25-41: LGTM! Clean removal of parsedProject dependency.

The refactor simplifies the data flow by using projectDefinition directly, aligning with the PR's objectives.

packages/project-builder-web/src/components/Console/index.tsx (1)

14-14: LGTM! Clean migration to tRPC.

The changes correctly migrate from the legacy API client to tRPC while maintaining the same functionality.

Also applies to: 44-44

packages/project-builder-lib/src/web/hooks/useProjectDefinition.ts (2)

8-14: Well-documented type definition.

The ProjectDefinitionSetter type is well-documented and clearly explains its purpose and behavior with Immer.


25-70: LGTM! Comprehensive interface with good documentation.

The UseProjectDefinitionResult interface is well-structured with clear JSDoc comments for each property. The new save methods with feedback enhance user experience.

packages/project-builder-server/src/compiler/admin/crud/displays.ts (1)

22-23: LGTM! Consistent use of ModelUtils.

The changes correctly migrate from using parsedProject.getModelById to ModelUtils.byIdOrThrow, maintaining consistent error handling across the codebase.

Also applies to: 54-55

packages/project-builder-web/src/app/ProjectDefinitionProvider/NewProjectCard.tsx (2)

21-21: LGTM! Improved error handling and async flow.

The changes correctly handle asynchronous project saving with proper error feedback using toast notifications.

Also applies to: 40-44


28-32: LGTM! Added loading state handling.

The component now correctly handles loading state during form submission, preventing multiple submissions.

Also applies to: 75-75

packages/project-builder-web/src/pages/data/models/edit/[id]/service.page.tsx (1)

27-29: LGTM! Form state management improvements.

The changes enhance form state handling by adding reset capability and simplifying the unsaved changes navigation hook parameters, making the code more maintainable.

packages/project-builder-web/src/pages/data/enums/edit/components/EnumHeaderBar.tsx (1)

22-23: LGTM! Enhanced save handling and UX.

The changes improve user experience by:

  • Preventing multiple submissions during save operations
  • Using a consistent approach to saving definitions with feedback
  • Proper handling of navigation after successful deletion

Also applies to: 27-38, 79-79

packages/project-builder-web/src/app/ProjectSelectorGate/ProjectSelectorDialog.tsx (2)

14-24: LGTM! Clear and consistent naming improvements.

The renaming from "Chooser" to "Select" and the import path reorganization improve code clarity and maintainability.


48-51: LGTM! Better comment clarity.

Changed from "dummy" to "no-op" which better describes the function's purpose.

packages/project-builder-web/src/app/AppLayout/ProjectSyncModal.tsx (2)

13-13: LGTM! Simplified API dependencies.

The removal of FilePayload and lastModifiedAt aligns with the PR's objective to streamline the sync flow.

Also applies to: 25-26


32-43:

Details

❓ Verification inconclusive

Verify sync behavior with simplified payload.

The sync flow has been simplified by removing the payload construction. Please ensure this doesn't affect sync functionality.


🏁 Script executed:

#!/bin/bash
# Check for any existing sync-related tests
rg -l "startSync.*test" 

Length of output: 25


SYNC FUNCTION VERIFICATION: Simplified Payload

The sync flow’s payload construction was removed, so please ensure that the removal does not impact the intended sync behavior. The automated search for sync-related tests (using the pattern "startSync.*test") yielded no results, which means we couldn’t automatically verify this change. It would be best to perform manual regression exercises focused on sync functionality.

  • Action Items:
    • Verify that the sync operation (i.e. startSync(currentProjectId)) functions as expected without the previously constructed payload.
    • Review and, if necessary, enhance test coverage for sync behavior to capture potential regressions.
    • Confirm that any expectations tied to the former payload are no longer applicable.
packages/project-builder-web/src/pages/apps/edit/BackendAppForm.tsx (3)

21-22: LGTM! Improved state management with feedback.

The use of saveDefinitionWithFeedback and isSavingDefinition provides better user feedback during save operations.


30-36: LGTM! Simplified form submission logic.

The form submission logic is now more straightforward and uses the new saveDefinitionWithFeedback pattern consistently.


88-90: LGTM! Better UX during save operations.

Disabling the save button while saving prevents duplicate submissions and provides clear visual feedback.

packages/project-builder-lib/src/compiler/app-compiler-spec.ts (1)

20-22:

Details

❓ Verification inconclusive

Verify the impact of return type changes.

The return type now allows for both single and array of GeneratorBundle. Please ensure all consumers handle both cases correctly.


🏁 Script executed:

#!/bin/bash
# Find usages of getChildrenForFeature
rg "getChildrenForFeature" -A 3

Length of output: 1167


Ensure proper handling of flexible return types in consumer code.

The update to the getChildrenForFeature return type now allows individual properties to be either a single GeneratorBundle or an array of them. Please verify that all consumers—especially in packages/project-builder-server/src/compiler/backend/feature.ts—properly handle both cases. Double-check that any downstream operations or type checks accommodate this dual possibility, and consider normalizing the output if a consistent type is expected.

packages/project-builder-web/src/app/ProjectDefinitionProvider/hooks/use-remote-project-definition-contents.ts (2)

10-14: LGTM! Well-structured interface definition.

The interface clearly defines the hook's return value with appropriate types.


16-24: LGTM! Clean state initialization.

Good practice using multiple useState hooks for separate concerns and destructuring useProjects hook values.

packages/project-builder-web/src/pages/apps/edit/WebAppForm.tsx (1)

21-30: LGTM! Clean hook usage and form setup.

Good practice using useResettableForm with zodResolver for validation.

packages/project-builder-server/src/compiler/backend/fastify.ts (2)

40-41: LGTM! Simplified dependency access.

Good refactoring to access projectDefinition and appCompiler directly from the builder.


102-102:

Details

❓ Verification inconclusive

Verify the impact of getRootChildren changes.

The removal of parsedProject.fastifyChildren in favor of appCompiler.getRootChildren() might affect the structure of generated bundles.


🏁 Script executed:

#!/bin/bash
# Search for any custom implementations of getRootChildren
ast-grep --pattern 'getRootChildren() {
  $$$
}'

Length of output: 50


Action Required: Manually verify the impact of the changes to getRootChildren

The initial AST search did not reveal any custom implementations of getRootChildren, but the absence of evidence here is not conclusive. Please run a manual review of the implementation details for getRootChildren to ensure that replacing parsedProject.fastifyChildren with appCompiler.getRootChildren() does not inadvertently affect the structure of the generated bundles.

  • File to review: packages/project-builder-server/src/compiler/backend/fastify.ts (specifically around line 102)
  • Next steps:
    • Confirm that appCompiler.getRootChildren() returns the correct structure needed for bundling.
    • Verify if there are any overlooked custom overrides or usage patterns in the codebase that could be impacted by this change.
packages/project-builder-lib/src/references/deserialize-schema.ts (1)

7-9: LGTM! Clean type imports.

Good practice importing types explicitly and using .js extension for imports.

packages/project-builder-web/src/app/ClientVersionProvider/ClientVersionProvider.test.tsx (2)

13-131: LGTM! Comprehensive test coverage with well-structured test cases.

The test suite effectively covers all critical aspects of the ClientVersionProvider component:

  • Version info fetching
  • Loading states
  • Error handling
  • Version change detection
  • Context provision

21-41: LGTM! Clean mock setup and teardown.

The beforeEach and afterEach hooks properly handle:

  • Mock initialization
  • WebSocket event cleanup
  • Global location object restoration
packages/project-builder-lib/src/references/types.ts (3)

5-15: LGTM! Well-documented class and methods.

The added JSDoc comments provide clear and comprehensive documentation for the DefinitionEntityType class and its methods.

Also applies to: 24-28, 33-38


57-92: LGTM! Clear and well-structured interface definitions.

The DefinitionEntity and DefinitionReference interfaces are well-documented and provide a robust structure for type safety.

Also applies to: 112-132


157-175: LGTM! Type-safe payload interface.

The ResolvedZodRefPayload interface provides a clear structure for handling resolved entity names with proper typing.

packages/project-builder-web/src/pages/settings/components/FeatureForm.tsx (2)

65-114: LGTM! Improved form submission with proper feedback handling.

The refactored submission logic using saveDefinitionWithFeedback provides better control over the success flow and error handling.


162-164: LGTM! Proper submit button state management.

The submit button is correctly disabled during form submission, preventing duplicate submissions.

packages/project-builder-web/src/pages/data/enums/hooks/useEnumForm.tsx (3)

90-107: LGTM! Improved duplicate name validation.

The code now properly validates enum names case-insensitively before saving, preventing duplicate names and providing clear error feedback.


109-141: LGTM! Enhanced save operation with feedback.

The new implementation using saveDefinitionWithFeedback provides:

  • Clear success messages
  • Proper state management during save operations
  • Consistent error handling

157-157: LGTM! Added loading state indicator.

The addition of isSavingDefinition to the return value allows components to show loading states during save operations.

packages/project-builder-web/src/pages/apps/edit/admin/crud/AdminCrudEmbeddedForm.tsx (1)

95-106: LGTM! Simplified model options retrieval.

The change from parsedProject.getModels() to directly accessing definition.models simplifies the code and aligns with the project's refactoring goals.

packages/project-builder-lib/src/parser/parser.ts (2)

12-12: LGTM! Enhanced type safety with ResolvedZodRefPayload.

The migration from ZodRefPayload to ResolvedZodRefPayload improves type safety for reference handling.

Also applies to: 126-126


133-133: LGTM! Improved schema parsing with references.

The switch to parseSchemaWithReferences provides a more robust way to handle schema references.

packages/project-builder-web/src/pages/plugins/PluginCard.tsx (3)

50-69: LGTM! Enhanced plugin enabling with feedback.

The new implementation using saveDefinitionWithFeedbackSync provides immediate feedback and prevents race conditions during plugin enabling.


73-82: LGTM! Enhanced plugin disabling with feedback.

Similar improvements to the disable operation, providing clear feedback and preventing race conditions.


117-123: LGTM! Added loading state to prevent double submissions.

The buttons are now properly disabled during save operations, preventing potential race conditions.

Also applies to: 133-139

packages/project-builder-lib/src/definition/project-definition-container.ts (2)

9-9: LGTM! Type safety improvement.

The change from ZodRefPayload to ResolvedZodRefPayload improves type safety by ensuring that references are resolved before being used.

Also applies to: 34-34, 41-41


106-109: LGTM! Enhanced serialization.

The change to use prettyStableStringify ensures consistent and readable serialization output.

packages/project-builder-server/src/server/plugin.ts (2)

61-67: LGTM! Improved context creation.

The change to use createContextBuilder with a structured configuration object improves code clarity and maintainability.


46-47:

Details

❓ Verification inconclusive

Verify service initialization timing.

The removal of await before service.init() could lead to race conditions if the initialization process includes asynchronous operations that need to complete before the service is used.


🏁 Script executed:

#!/bin/bash
# Search for async operations in service.init()
ast-grep --pattern 'init() {
  $$$
  await $$$
  $$$
}'

Length of output: 57


Service Initialization Timing Check

The recent search for any await statements within service.init() returned no evidence of asynchronous operations. However, since the initialization was previously awaited, please manually verify that no asynchronous logic is performed within service.init(). If asynchronous operations are present in the initialization process, the omission of await might lead to race conditions.

  • File: packages/project-builder-server/src/server/plugin.ts (lines 46-47)
  • Action Required: Confirm that service.init() does not perform asynchronous operations that require completion before subsequent service usage. If such operations exist, reintroduce the await to ensure correct service initialization timing.
packages/project-builder-server/src/compiler/backend/services.ts (1)

3-3: LGTM! Type refinement.

The change from ParsedModel to ModelConfig aligns with the broader refactor to use more specific and descriptive types.

Also applies to: 73-73, 93-93

packages/project-builder-web/src/pages/apps/edit/admin/AdminEditSectionForm.tsx (2)

41-47: LGTM! Enhanced state management.

The destructured hooks provide better control over the saving process and UI feedback.


183-185: LGTM! Improved button state.

The disabled state during saving prevents duplicate submissions and provides clear feedback to users.

packages/project-builder-lib/src/references/resolve-zod-ref-payload-names.unit.test.ts (1)

1-227: Great test coverage!

The test suite is well-structured and comprehensive, covering essential scenarios:

  • Simple entity names without dependencies
  • Entity names with dependencies in correct order
  • Array dependencies
  • Error handling for unresolvable dependencies
  • Invalid references handling
  • Reference name resolution skipping
  • Input data preservation
packages/project-builder-lib/src/schema/models/index.ts (1)

147-147: LGTM! Improved name resolution.

The change from static namePath to dynamic getNameResolver aligns with the PR objectives and provides more flexibility in resolving foreign relation names.

packages/project-builder-lib/src/references/ref-builder.unit.test.ts (1)

1-225: Excellent test coverage!

The test suite is well-structured and comprehensive, covering essential scenarios:

  • Simple schema parsing
  • Entity collection and reference resolution
  • Nested and parent references
  • Chaining of references
  • Custom name resolvers
  • Name resolvers with dependencies

The tests are well-documented with clear descriptions and comments explaining the purpose of each test case.

packages/project-builder-web/src/pages/settings/theme-builder.tsx (1)

27-44: LGTM! Improved save flow.

The changes align well with the PR objectives:

  • Replaced setConfigAndFixReferences with saveDefinitionWithFeedback
  • Simplified error handling by leveraging the new feedback mechanism
  • Improved code clarity by using more descriptive variable names
packages/project-builder-web/package.json (1)

44-50:

Details

✅ Verification successful

LGTM! Testing dependencies added.

The addition of testing libraries and utilities enhances the project's testing capabilities. The fixed versions ensure reproducibility.

Let's verify if these are the latest stable versions of the testing libraries:

Also applies to: 62-62


🌐 Web query:

What are the latest versions of @testing-library/react, @testing-library/dom, @testing-library/jest-dom, and @testing-library/user-event?

💡 Result:

Here are the latest versions of key Testing Library packages based on available information as of February 2025:

Core Libraries

1️⃣ @testing-library/react
Latest: v16.2.0 (Released 2025-01-15)
Key changes in recent major versions:

  • Requires React 18+ and React DOM 18+[6]
  • Now uses @testing-library/dom as peer dependency[6]

2️⃣ @testing-library/dom
Latest: v10.4.0 (Released 2024-07-22)
Recent updates include:

  • Dropped Node.js <18 support[7]
  • Updated ARIA role handling[7]

Utilities

3️⃣ @testing-library/jest-dom
Latest: v6.6.3 (Released October 2024)
Key features:

  • Extended Jest/Vitest matchers like .toBeVisible() and .toBeDisabled()[3][8]
  • Supports both Jest and Vitest via separate imports[8]

4️⃣ @testing-library/user-event
Current recommendation: v14+
Key updates since v13:

  • Improved async interaction handling[9]
  • Requires explicit setup with userEvent.setup()[9]
  • Better browser interaction simulations than fireEvent[9]

Migration Notes

# Recommended installation command for new projects:
npm install --save-dev @testing-library/react@16 @testing-library/dom@10 @testing-library/jest-dom@6 @testing-library/user-event@14

Important compatibility notes:
• React Testing Library v16 requires React 18+/DOM 18+[6][10]
• User Event v14 fixes async interaction warnings present in v13[5][9]
• Jest DOM works with both Jest and Vitest via separate import paths[8]

(Version data sourced from official release notes and package registries)[1][3][6][7][9]

Citations:


LGTM! Testing dependencies are correctly updated to the latest stable versions.
The versions listed in packages/project-builder-web/package.json (lines 44-50 and 62) are in line with the latest release data as of February 2025:

  • @testing-library/react: 16.2.0
  • @testing-library/dom: 10.4.0
  • @testing-library/jest-dom: 6.6.3
  • @testing-library/user-event: 14.6.1 (aligns with the current recommendation for v14+)

These fixed versions enhance reproducibility and ensure the project's testing capabilities remain robust.

packages/project-builder-web/public/preview-app.json (6)

12-12: Consistent renaming of fields to use "Ref" suffix.

The renaming of fields to include the "Ref" suffix (e.g., featureRef, modelFieldRef, etc.) improves clarity and better indicates references.

Also applies to: 17-17, 25-25, 29-29, 37-37, 43-43, 51-51, 79-79, 95-95, 120-120, 126-126, 131-134, 137-140, 147-147


1256-1297: Well-structured file storage plugin configuration.

The storage plugin configuration is well-organized with:

  • Clear category definitions for different file types
  • Appropriate file size limits
  • Role-based upload permissions
  • S3 adapter configuration with environment variables

31-34: New "Cover Photo" field added with proper file relation.

The addition of the cover photo field is well-structured with:

  • Clear label
  • Proper model relation reference
  • Appropriate file type

193-226: Enhanced auth configuration with improved role definitions.

The auth configuration improvements include:

  • Clear role definitions with helpful comments
  • Built-in flag for system roles
  • Proper references to user and role models

302-320: Comprehensive GraphQL configuration for models.

The GraphQL configuration for each model is well-structured with:

  • Clear field visibility control
  • Role-based mutation permissions
  • Proper relation exposure

Also applies to: 683-707, 819-835, 903-919, 1030-1037, 1101-1115, 1201-1208


1300-1300:

Details

✅ Verification successful

Schema version increment from 4 to 9.

Major schema version increment suggests significant structural changes. Please ensure proper migration scripts are in place.

Let's check for migration scripts:


🏁 Script executed:

#!/bin/bash
# Description: Check for migration scripts between schema versions 4 and 9

# Search for migration files
fd -t f "migration" -x echo "Found migration file: {}"

# Search for version-related code
rg -A 5 "schemaVersion.*[49]"

Length of output: 1891


Migration Scripts Are Present for Schema Version Change

The schema version bump from 4 to 9 implies significant changes. The verification indicates that multiple migration scripts (e.g., migration-009-rename-refs.ts, along with scripts numbered 005 to 008) exist within the packages/project-builder-lib/src/migrations/ directory. Additionally, corresponding unit tests are present and configurations in files like tests/simple/baseplate/project-definition.json reflect schema version 9. Please ensure that all individual migration steps (covering the gap from version 4 to 9) are thoroughly validated and that migration tests pass in your CI pipeline.

Comment on lines +34 to +37
// a bit of a hack to make sure we get the latest form state before continuing
// there's not an easy way of figuring out if the submission was successful or not
// so we wait for a React re-render and then check if the form is still dirty
await new Promise((resolve) => setTimeout(resolve, 1));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider a more robust approach for handling form submission state.

The current approach of waiting 1ms might be too short for React's batching behavior to complete, potentially leading to race conditions. The comment acknowledges this as a "hack", suggesting room for improvement.

Consider these alternatives:

  1. Use React Hook Form's submission status:
const { isSubmitting, isSubmitSuccessful } = useFormState({ control });
  1. Or handle submission status explicitly:
const onContinue = async () => {
  try {
    await onSubmit();
    return true;
  } catch {
    return false;
  }
};

Comment on lines +14 to +39
export const projectsRouter = router({
list: privateProcedure.query(async ({ ctx }) =>
Promise.all(
ctx.services.map(async (service): Promise<ProjectInfo> => {
const { contents } = await service.readDefinition();
const parsedContents = JSON.parse(contents) as unknown;

if (
!parsedContents ||
typeof parsedContents !== 'object' ||
!('name' in parsedContents) ||
typeof parsedContents.name !== 'string'
) {
throw new Error(
`Invalid project definition for ${service.directory}`,
);
}

return router({
list: privateProcedure.query(async () =>
Promise.all(
services.map(
async (
service,
): Promise<{ id: string; name: string; directory: string }> => {
const config = await service.readConfig();
if (!config) {
throw new Error(`File config missing for ${service.directory}`);
}
const parsedContents = JSON.parse(
config.contents,
) as ProjectDefinition;
return {
id: service.id,
name: parsedContents.name,
directory: service.directory,
};
},
),
),
return {
id: service.id,
name: parsedContents.name,
directory: service.directory,
};
}),
),
),
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle JSON parsing errors gracefully.

In list, the code currently trusts JSON.parse to work without issue. If the file contents are malformed, this will throw a runtime error. Consider wrapping the parsing in a try/catch block to provide a friendlier error or fallback behavior.

     const { contents } = await service.readDefinition();
-    const parsedContents = JSON.parse(contents) as unknown;
+    let parsedContents: unknown;
+    try {
+      parsedContents = JSON.parse(contents);
+    } catch (parseError) {
+      throw new Error(`Could not parse project definition for ${service.directory}: ${parseError}`);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const projectsRouter = router({
list: privateProcedure.query(async ({ ctx }) =>
Promise.all(
ctx.services.map(async (service): Promise<ProjectInfo> => {
const { contents } = await service.readDefinition();
const parsedContents = JSON.parse(contents) as unknown;
if (
!parsedContents ||
typeof parsedContents !== 'object' ||
!('name' in parsedContents) ||
typeof parsedContents.name !== 'string'
) {
throw new Error(
`Invalid project definition for ${service.directory}`,
);
}
return router({
list: privateProcedure.query(async () =>
Promise.all(
services.map(
async (
service,
): Promise<{ id: string; name: string; directory: string }> => {
const config = await service.readConfig();
if (!config) {
throw new Error(`File config missing for ${service.directory}`);
}
const parsedContents = JSON.parse(
config.contents,
) as ProjectDefinition;
return {
id: service.id,
name: parsedContents.name,
directory: service.directory,
};
},
),
),
return {
id: service.id,
name: parsedContents.name,
directory: service.directory,
};
}),
),
),
export const projectsRouter = router({
list: privateProcedure.query(async ({ ctx }) =>
Promise.all(
ctx.services.map(async (service): Promise<ProjectInfo> => {
const { contents } = await service.readDefinition();
let parsedContents: unknown;
try {
parsedContents = JSON.parse(contents);
} catch (parseError) {
throw new Error(`Could not parse project definition for ${service.directory}: ${parseError}`);
}
if (
!parsedContents ||
typeof parsedContents !== 'object' ||
!('name' in parsedContents) ||
typeof parsedContents.name !== 'string'
) {
throw new Error(
`Invalid project definition for ${service.directory}`,
);
}
return {
id: service.id,
name: parsedContents.name,
directory: service.directory,
};
}),
),
),
});

Comment on lines +25 to +38
onConsoleEmitted: websocketProcedure
.input(z.object({ id: z.string() }))
.subscription(({ input: { id }, ctx }) =>
observable<CommandConsoleEmittedPayload>((emit) => {
const unsubscribe = ctx
.getApi(id)
.on('command-console-emitted', (payload) => {
emit.next(payload);
});
return () => {
unsubscribe();
};
}),

onConsoleEmitted: websocketProcedure
.input(z.object({ id: z.string() }))
.subscription(({ input: { id } }) =>
observable<CommandConsoleEmittedPayload>((emit) => {
const unsubscribe = getApi(id).on(
'command-console-emitted',
(payload) => {
emit.next(payload);
},
);
return () => {
unsubscribe();
};
}),
),
});
}
),
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for getApi.

The subscription should handle potential errors from getApi to prevent unhandled exceptions.

   .subscription(({ input: { id }, ctx }) =>
     observable<CommandConsoleEmittedPayload>((emit) => {
+      try {
         const unsubscribe = ctx
           .getApi(id)
           .on('command-console-emitted', (payload) => {
             emit.next(payload);
           });
         return () => {
           unsubscribe();
         };
+      } catch (error) {
+        ctx.logger.error('Failed to setup console subscription:', error);
+        emit.error(new Error('Failed to setup console subscription'));
+        return () => {};
+      }
     }),
   ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onConsoleEmitted: websocketProcedure
.input(z.object({ id: z.string() }))
.subscription(({ input: { id }, ctx }) =>
observable<CommandConsoleEmittedPayload>((emit) => {
const unsubscribe = ctx
.getApi(id)
.on('command-console-emitted', (payload) => {
emit.next(payload);
});
return () => {
unsubscribe();
};
}),
onConsoleEmitted: websocketProcedure
.input(z.object({ id: z.string() }))
.subscription(({ input: { id } }) =>
observable<CommandConsoleEmittedPayload>((emit) => {
const unsubscribe = getApi(id).on(
'command-console-emitted',
(payload) => {
emit.next(payload);
},
);
return () => {
unsubscribe();
};
}),
),
});
}
),
onConsoleEmitted: websocketProcedure
.input(z.object({ id: z.string() }))
.subscription(({ input: { id }, ctx }) =>
observable<CommandConsoleEmittedPayload>((emit) => {
try {
const unsubscribe = ctx
.getApi(id)
.on('command-console-emitted', (payload) => {
emit.next(payload);
});
return () => {
unsubscribe();
};
} catch (error) {
ctx.logger.error('Failed to setup console subscription:', error);
emit.error(new Error('Failed to setup console subscription'));
return () => {};
}
}),
),

Comment on lines +15 to +23
const api = ctx.getApi(id);

api.buildProject().catch((error: unknown) => {
logger.error(error);
});
api.buildProject().catch((error: unknown) => {
ctx.logger.error(error);
});

return { success: true, writeResult };
return { success: true };
}),

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling and response structure.

The current implementation has room for improvement in error handling and response structure.

   .mutation(({ input: { id }, ctx }) => {
     const api = ctx.getApi(id);

-    api.buildProject().catch((error: unknown) => {
-      ctx.logger.error(error);
-    });
+    return api.buildProject()
+      .then(() => ({ success: true, status: 'completed' }))
+      .catch((error: unknown) => {
+        ctx.logger.error('Build project failed:', error);
+        throw new Error('Failed to build project');
+      });

-    return { success: true };
   }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.mutation(({ input: { id }, ctx }) => {
const api = ctx.getApi(id);
api.buildProject().catch((error: unknown) => {
logger.error(error);
});
api.buildProject().catch((error: unknown) => {
ctx.logger.error(error);
});
return { success: true, writeResult };
return { success: true };
}),
.mutation(({ input: { id }, ctx }) => {
const api = ctx.getApi(id);
return api.buildProject()
.then(() => ({ success: true, status: 'completed' }))
.catch((error: unknown) => {
ctx.logger.error('Build project failed:', error);
throw new Error('Failed to build project');
});
}),

Comment on lines +38 to 43
const { migratedDefinition, appliedMigrations } = runSchemaMigrations(
projectJson as ProjectDefinition,
);
if (appliedMigrations.length > 0) {
await writeFile(
projectJsonPath,
prettyStableStringify(migratedProjectJson),
);
await writeFile(projectJsonPath, prettyStableStringify(migratedDefinition));
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify type assertion safety.

The type assertion as ProjectDefinition could be unsafe. Consider adding runtime validation before the assertion.


🏁 Script executed:

#!/bin/bash
# Search for other instances of this type assertion to ensure consistent validation
rg -A 2 "as ProjectDefinition" 

Length of output: 555


Action Required: Integrate Runtime Validation for ProjectDefinition Instances

The type assertions in use (e.g., converting projectJson as ProjectDefinition in packages/project-builder-server/src/runner/index.ts and casting the result of migration.migrate(migratedDefinition) in packages/project-builder-lib/src/migrations/index.ts) currently rely solely on compile-time checks and lack any runtime verification. This approach may lead to unsafe assumptions about the structure of the data.

  • Runner Module (packages/project-builder-server/src/runner/index.ts):
    • Validate that projectJson conforms to the ProjectDefinition schema before asserting its type.
  • Migrations Module (packages/project-builder-lib/src/migrations/index.ts):
    • After performing migration via migration.migrate(migratedDefinition), ensure the result meets the expectations of ProjectDefinition with runtime validation.

Recommendation: Consider using a runtime schema validation library (such as zod or io-ts) to perform these checks and safeguard against invalid data shapes.

Comment on lines +41 to +50
const orderedEntities = toposort.array(
entitiesWithNameResolver.map((entity) => entity.id),
entitiesWithNameResolver.flatMap((entity) => {
const entityIds = entity.nameResolver.idsToResolve ?? {};
return Object.values(entityIds)
.flat()
.filter((id) => id in entitiesById)
.map((id) => [id, entity.id] as [string, string]);
}),
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for cyclic dependencies.

The toposort operation could fail with cyclic dependencies. Consider wrapping it in a try-catch block to provide a more user-friendly error message.

-  const orderedEntities = toposort.array(
+  let orderedEntities: string[];
+  try {
+    orderedEntities = toposort.array(
       entitiesWithNameResolver.map((entity) => entity.id),
       entitiesWithNameResolver.flatMap((entity) => {
         const entityIds = entity.nameResolver.idsToResolve ?? {};
         return Object.values(entityIds)
           .flat()
           .filter((id) => id in entitiesById)
           .map((id) => [id, entity.id] as [string, string]);
       }),
     );
+  } catch (error) {
+    throw new Error('Cyclic dependency detected in entity references. Please check your entity relationships.');
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const orderedEntities = toposort.array(
entitiesWithNameResolver.map((entity) => entity.id),
entitiesWithNameResolver.flatMap((entity) => {
const entityIds = entity.nameResolver.idsToResolve ?? {};
return Object.values(entityIds)
.flat()
.filter((id) => id in entitiesById)
.map((id) => [id, entity.id] as [string, string]);
}),
);
let orderedEntities: string[];
try {
orderedEntities = toposort.array(
entitiesWithNameResolver.map((entity) => entity.id),
entitiesWithNameResolver.flatMap((entity) => {
const entityIds = entity.nameResolver.idsToResolve ?? {};
return Object.values(entityIds)
.flat()
.filter((id) => id in entitiesById)
.map((id) => [id, entity.id] as [string, string]);
}),
);
} catch (error) {
throw new Error('Cyclic dependency detected in entity references. Please check your entity relationships.');
}

Comment on lines +92 to +95
const { definition, definitionContainer } = useProjectDefinition();
const model = modelRef
? ModelUtils.byIdOrThrow(definition, modelRef)
: undefined;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify error handling for ModelUtils.byIdOrThrow.

The switch from getModelById to ModelUtils.byIdOrThrow introduces stricter error handling. Ensure that error boundaries or try-catch blocks are in place to handle potential exceptions when the model is not found.


🏁 Script executed:

#!/bin/bash
# Search for error boundary components or try-catch blocks in parent components
rg -A 5 "ErrorBoundary|try\s*{" packages/project-builder-web/src/pages/apps/edit/admin/crud/

Length of output: 200


Next Steps: Verify Error Handling for ModelUtils.byIdOrThrow Usage

The switch to using ModelUtils.byIdOrThrow means that an exception will be raised when the model is not found. Our initial attempt to locate error boundaries or try-catch blocks in the parent components (under packages/project-builder-web/src/pages/apps/edit/admin/crud/) encountered a regex error. To move forward, please run the following revised shell script to check for any existing error handling constructs:

Once you have the output:

  • If there are try-catch blocks or an ErrorBoundary wrapping the code that calls ModelUtils.byIdOrThrow, please confirm that they are adequate to catch the potential exceptions.
  • If not, consider adding localized error handling (e.g., wrapping the call in a try-catch) or ensuring that a higher-level ErrorBoundary component is in place to handle these exceptions.

Please verify the results and update the error handling accordingly.


🏁 Script executed:

#!/bin/bash
echo "Searching for try-catch blocks in admin/crud directory:"
rg -A 5 'try\s*\{' packages/project-builder-web/src/pages/apps/edit/admin/crud/
echo "Searching for ErrorBoundary usage in admin/crud directory:"
rg -A 5 'ErrorBoundary' packages/project-builder-web/src/pages/apps/edit/admin/crud/

Length of output: 414


Enhance Error Handling for ModelUtils.byIdOrThrow

Our investigation shows that no try-catch blocks or ErrorBoundary components are present in the packages/project-builder-web/src/pages/apps/edit/admin/crud/ directory to capture exceptions thrown by ModelUtils.byIdOrThrow. This leaves the current implementation exposed to unhandled errors if the model is not found.

Suggestions:

  • Wrap the call in a try-catch block: At the point where ModelUtils.byIdOrThrow is invoked (lines 92-95 in CrudTableColumnsForm.tsx), add error handling to catch potential exceptions.
  • Or Use an ErrorBoundary: Consider implementing or ensuring an existing ErrorBoundary component wraps this part of the UI to gracefully handle errors and prevent the entire app from crashing.

Please update the error handling to address this potential issue.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (10)
.changeset/strange-toes-attend.md (1)

5-6: Consider enhancing the changeset description.

While the description captures the core change, it would be helpful to include:

  • Motivation for the change
  • Impact on existing code
  • Any migration steps if needed

Apply this diff to enhance the description:

-Refactor nameRefs to use resolveName for resolving name of entities
+Refactor nameRefs to use resolveName for resolving name of entities.
+
+This change:
+- Improves code clarity by centralizing name resolution logic
+- Maintains backward compatibility with existing implementations
+- Aligns with the new project definition sync/save flow
packages/project-builder-web/src/services/api/errors.ts (1)

13-28: Consider adding type narrowing for better type safety.

The function is well-structured with proper documentation and error handling. However, the optional chaining on err.data?.code could be improved with type narrowing.

Consider this improvement:

 export function createProjectNotFoundHandler(
   projectId: string,
 ): (err: unknown) => never {
   return (err: unknown) => {
-    if (isTRPCClientError(err) && err.data?.code === 'NOT_FOUND') {
+    if (isTRPCClientError(err) && 'data' in err && err.data && err.data.code === 'NOT_FOUND') {
       throw new ProjectNotFoundError(projectId);
     }
     throw err;
   };
 }
packages/project-builder-web/src/pages/data/models/_hooks/useModelForm.ts (2)

102-127: Enhance error messages for better user guidance.

While the validation logic is thorough, the error messages could be more descriptive and provide guidance on how to fix the issues.

Apply this diff to improve error messages:

-          toast.error('Model must have at least one field.');
+          toast.error('Model validation failed: At least one field is required. Please add a field to continue.');
-          toast.error('Model must have at least one primary key field.');
+          toast.error('Model validation failed: A primary key field is required for database operations. Please select at least one field as primary key.');
-            message: `Model with name ${updatedModel.name} already exists.`,
+            message: `A model named "${updatedModel.name}" already exists. Please choose a unique name.`,

129-164: Refactor service method validation to reduce duplication.

The validation logic for service methods could be improved by:

  1. Extracting duplicated validation logic into a helper function.
  2. Enhancing error messages with more context.

Apply this diff to improve the code:

+        // Helper function to validate service method
+        const validateServiceMethod = (
+          method: { enabled?: boolean; fields?: string[]; transformerNames?: string[] },
+          methodName: string,
+        ): boolean => {
+          if (method?.enabled && !method.fields?.length && !method.transformerNames?.length) {
+            toast.error(
+              `${methodName} method validation failed: At least one field or transformer is required.` +
+              ` Please configure the ${methodName.toLowerCase()} method or disable it.`
+            );
+            return false;
+          }
+          return true;
+        };
+
         // clear out any service methods that are disabled
         const { service } = updatedModel;
         if (service) {
-          if (service.create?.enabled) {
-            if (
-              !service.create.fields?.length &&
-              !service.create.transformerNames?.length
-            ) {
-              toast.error(
-                'Create method must have at least one field or transformer.',
-              );
-              return;
-            }
-          } else {
+          if (!validateServiceMethod(service.create, 'Create')) {
+            return;
+          }
+          if (!service.create?.enabled) {
             service.create = undefined;
           }
-          if (service.update?.enabled) {
-            if (
-              !service.update.fields?.length &&
-              !service.update.transformerNames?.length
-            ) {
-              toast.error(
-                'Update method must have at least one field or transformer.',
-              );
-              return;
-            }
-          } else {
+          if (!validateServiceMethod(service.update, 'Update')) {
+            return;
+          }
+          if (!service.update?.enabled) {
             service.update = undefined;
           }
packages/tools/eslint-configs/tailwind.js (1)

23-24: Consider documenting the VSCode error.

The comment about VSCode throwing an error is helpful, but consider documenting the specific error and its workaround in your project's documentation for future reference.

.changeset/six-candles-accept.md (1)

7-8: Enhance the changeset description.

While the current description captures the main purpose, it would be helpful to include:

  1. The motivation behind the changes
  2. The specific improvements made to the sync/save flow
  3. The impact on consumers of these packages

Consider expanding the description like this:

-Rewrite saving/syncing logic to clean up flows for parsing and saving project definition
+Rewrite saving/syncing logic to improve project definition handling:
+- Implement asynchronous saving capabilities
+- Refactor nameRefs to name resolvers for Zod reference payloads
+- Create clearer flow for parsing and saving project definitions
+
+This change improves code maintainability and provides a more intuitive API for consumers.
packages/project-builder-lib/src/tools/model-merger/model-merger.ts (4)

77-107: Consider optimizing array operations for better performance.

The current implementation has O(n²) time complexity due to nested array operations (find, some). For large arrays, this could impact performance.

Consider using a Map for O(1) lookups:

 function diffByKey<T>(
   current: T[],
   desired: T[],
   getKey: (item: T) => string,
   options: ModelDiffOptions = {},
 ): DiffOperation<T>[] {
   const ops: DiffOperation<T>[] = [];
+  const currentMap = new Map(current.map(item => [getKey(item), item]));
+  const desiredKeys = new Set(desired.map(getKey));

   for (const desiredItem of desired) {
     const key = getKey(desiredItem);
-    const currentItem = current.find((item) => getKey(item) === key);
+    const currentItem = currentMap.get(key);
     if (!currentItem) {
       ops.push({ type: 'add', key, item: desiredItem });
       continue;
     }
     if (!isMatch(currentItem, desiredItem)) {
       ops.push({ type: 'update', key, item: desiredItem });
     }
   }

   if (options.enableRemove) {
-    for (const currentItem of current) {
-      const key = getKey(currentItem);
-      if (!desired.some((item) => getKey(item) === key)) {
+    for (const [key, currentItem] of currentMap) {
+      if (!desiredKeys.has(key)) {
         ops.push({ type: 'remove', key, item: currentItem });
       }
     }
   }

   return ops;
 }

118-151: Consider enhancing error handling and immutability.

While the function works correctly, there are two areas for improvement:

  1. The function mutates the array in-place during splice operations
  2. Error handling is only implemented for the update operation

Consider this more robust implementation:

 function applyPatchByKey<TConfig, TInput>(
   current: TConfig[],
   patch: DiffOperation<TInput>[],
   getKey: (item: TConfig) => string,
   assignId: (item: TInput, previousItem?: TConfig) => TConfig,
 ): TConfig[] {
-  const items = [...current];
+  let items = [...current];
   for (const { type, key, item } of patch) {
     const index = items.findIndex((i) => getKey(i) === key);
     switch (type) {
       case 'add': {
-        items.push(assignId(item));
+        items = [...items, assignId(item)];
         break;
       }
       case 'update': {
         if (index === -1) {
           throw new Error(
             `Cannot apply patch. Item with key "${key}" not found.`,
           );
         }
         // Preserve existing id if present.
-        items[index] = assignId(item, items[index]);
+        items = [
+          ...items.slice(0, index),
+          assignId(item, items[index]),
+          ...items.slice(index + 1),
+        ];
         break;
       }
       case 'remove': {
         if (index === -1) {
+          throw new Error(
+            `Cannot remove item. Item with key "${key}" not found.`,
+          );
+        }
-        if (index !== -1) {
-          items.splice(index, 1);
-        }
+        items = [...items.slice(0, index), ...items.slice(index + 1)];
         break;
       }
     }
   }
   return items;
 }

159-164: Consider making the unique constraint key more robust.

The current implementation might have issues if field references contain the pipe character used as a separator.

Consider using a more robust separator or encoding:

 function getUniqueConstraintKey(
   constraint: ModelUniqueConstraintDefinitionInput,
 ): string {
   const fields = constraint.fields.map((f) => f.fieldRef).sort();
-  return fields.join('|');
+  // Use a character that's unlikely to appear in field names
+  return fields.join('\u0000');
   // Alternative: Use JSON.stringify for complete safety
+  // return JSON.stringify(fields);
 }

249-323: Optimize ID resolution for better performance.

The current implementation performs repeated array searches for ID resolution. This could be optimized by caching the field lookups.

Consider this optimization:

 export function applyModelPatchInPlace(
   current: ModelConfig['model'],
   patch: ModelDiffOutput,
   definitionContainer: ProjectDefinitionContainer,
 ): void {
+  // Cache field lookups
+  const fieldMap = new Map(current.fields.map(f => [f.name, f]));
+
   // Resolve relation references.
   const resolveLocalId = (name: string): string => {
-    const field = current.fields.find((f) => f.name === name);
+    const field = fieldMap.get(name);
     return field ? field.id : name;
   };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Lite

📥 Commits

Reviewing files that changed from the base of the PR and between 3e24f5a and e0a068f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • .changeset/six-candles-accept.md (1 hunks)
  • .changeset/strange-toes-attend.md (1 hunks)
  • packages/project-builder-lib/src/compiler/app-compiler-spec.ts (1 hunks)
  • packages/project-builder-lib/src/tools/model-merger/model-merger.ts (1 hunks)
  • packages/project-builder-lib/src/utils/merge.ts (0 hunks)
  • packages/project-builder-web/package.json (2 hunks)
  • packages/project-builder-web/src/pages/data/models/_hooks/useModelForm.ts (4 hunks)
  • packages/project-builder-web/src/services/api/errors.ts (1 hunks)
  • packages/project-builder-web/src/services/config.ts (1 hunks)
  • packages/tools/eslint-configs/tailwind.js (1 hunks)
💤 Files with no reviewable changes (1)
  • packages/project-builder-lib/src/utils/merge.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/project-builder-web/src/services/config.ts
  • packages/project-builder-web/package.json
  • packages/project-builder-lib/src/compiler/app-compiler-spec.ts
🔇 Additional comments (7)
.changeset/strange-toes-attend.md (1)

1-3: LGTM! Patch version bump is appropriate.

The patch version bump aligns with semantic versioning guidelines for backward-compatible refactors.

packages/project-builder-web/src/services/api/errors.ts (2)

1-1: LGTM!

The import statement is correctly defined and used within the file.


3-11: LGTM! Well-structured error class.

The class follows best practices by:

  • Extending the Error base class
  • Including a descriptive error message
  • Setting the name property for proper error identification
  • Having proper JSDoc documentation
packages/project-builder-web/src/pages/data/models/_hooks/useModelForm.ts (2)

23-23: LGTM! Improved error handling with custom error type.

Good use of a custom error type for better error handling and user feedback.


166-196: LGTM! Well-structured save and navigation logic.

The implementation provides:

  • Clear success messages based on the operation type (create/update)
  • Proper form reset and navigation after successful submission
  • Appropriate callback handling
.changeset/six-candles-accept.md (1)

2-4:

Details

✅ Verification successful

Verify if patch version is appropriate for these changes.

The PR involves a significant rewrite of sync/save flows. While the changes aim to improve clarity, please verify that they maintain backward compatibility and don't introduce breaking changes that would require a minor or major version bump.

Run this script to check for potential breaking changes:


🏁 Script executed:

#!/bin/bash
# Description: Check for potential breaking changes in the affected packages

# Look for removed or renamed exports
rg -i "export.*\b(class|interface|type|function|const)\b.*\b(ProjectDefinition|nameRefs|ParsedProjectDefinition)\b"

# Look for signature changes in public APIs
ast-grep --pattern $'export (class|interface|type|function|const) $_ {
  $$$
}'

Length of output: 874


Patch Bump Confirmed – Consider Enhancing Description Details

  • The verification scripts show that all exported APIs (e.g., the functions and types linked with ProjectDefinition) remain unchanged, indicating no breaking changes.
  • Since no removed or signature-altered exports were found, the patch version bumps for the affected packages are appropriate.
  • It could be helpful to expand the change description in this changeset to clarify the impact of the sync/save flow rewrite.
packages/project-builder-lib/src/tools/model-merger/model-merger.ts (1)

1-67: LGTM! Well-structured type definitions.

The type definitions are clear, comprehensive, and follow TypeScript best practices. The use of the Omit utility type effectively prevents ID field conflicts in input types.

Comment on lines +20 to +34
'tailwindcss/no-custom-classname': [
'error',
{
// for some reason, VSCode throws an error for this rule so adding the rules manually
whitelist: [
'text-muted-foreground',
'text-destructive',
'text-style-muted',
'bg-background',
'bg-muted',
'border-border',
'text-style-prose',
],
},
],
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

LGTM! Verify whitelist against Tailwind config.

The whitelist configuration looks good and follows consistent naming patterns. However, ensure these classes are properly defined in your Tailwind configuration file.

Let's verify the class definitions in your Tailwind configuration:


🏁 Script executed:

#!/bin/bash
# Search for the Tailwind configuration file and check for these class definitions
fd -e js -e ts "tailwind.config" -x cat {} | grep -E "text-muted-foreground|text-destructive|text-style-muted|bg-background|bg-muted|border-border|text-style-prose"

Length of output: 228


Action Required: Verify and update whitelist entries in ESLint config.

The ESLint rule for tailwindcss/no-custom-classname is correctly structured, and the comment on the VSCode issue is useful. However, a search of the Tailwind configuration revealed that only the classes corresponding to text-style-muted and text-style-prose are defined—while no definitions were found for text-muted-foreground, text-destructive, bg-background, bg-muted, or border-border. Please verify whether these additional classes are intended to be recognized (perhaps defined elsewhere or pending definition) or if they should be removed/updated to align with the Tailwind configuration.

Comment on lines +175 to +240
export function diffModel(
current: ModelConfig['model'],
desired: ModelDefinitionInput,
definitionContainer: ProjectDefinitionContainer,
options?: ModelDiffOptions,
): ModelDiffOutput | undefined {
// Resolve relation references.
const resolvedRelations =
current.relations?.map((relation) => ({
...relation,
references: relation.references.map((reference) => ({
...reference,
localRef: definitionContainer.nameFromId(reference.localRef),
foreignRef: definitionContainer.nameFromId(reference.foreignRef),
})),
})) ?? [];

const fieldDiffs = diffByKey(
current.fields,
desired.fields,
(f) => f.name,
options,
);
const relationDiffs = diffByKey(
resolvedRelations,
desired.relations ?? [],
(r) => r.name,
options,
);
const uniqueConstraintDiffs = diffByKey(
(current.uniqueConstraints ?? []).map((constraint) => ({
...constraint,
fields: constraint.fields.map((f) => ({
...f,
fieldRef: definitionContainer.nameFromId(f.fieldRef),
})),
})),
desired.uniqueConstraints ?? [],
getUniqueConstraintKey,
options,
);

const pkDiff = isEqual(
current.primaryKeyFieldRefs.map((ref) =>
definitionContainer.nameFromId(ref),
),
desired.primaryKeyFieldRefs,
)
? undefined
: desired.primaryKeyFieldRefs;

if (
fieldDiffs.length > 0 ||
relationDiffs.length > 0 ||
uniqueConstraintDiffs.length > 0 ||
pkDiff
) {
return {
fields: fieldDiffs,
relations: relationDiffs,
uniqueConstraints: uniqueConstraintDiffs,
primaryKeyFieldRefs: pkDiff,
};
}
return undefined;
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add error handling for name resolution failures.

The name resolution using definitionContainer.nameFromId could fail silently if an ID doesn't exist.

Let's verify the name resolution behavior:

Consider adding error handling:

 export function diffModel(
   current: ModelConfig['model'],
   desired: ModelDefinitionInput,
   definitionContainer: ProjectDefinitionContainer,
   options?: ModelDiffOptions,
 ): ModelDiffOutput | undefined {
+  const safeNameFromId = (id: string): string => {
+    const name = definitionContainer.nameFromId(id);
+    if (!name) {
+      throw new Error(`Failed to resolve name for ID: ${id}`);
+    }
+    return name;
+  };
+
   // Resolve relation references.
   const resolvedRelations =
     current.relations?.map((relation) => ({
       ...relation,
       references: relation.references.map((reference) => ({
         ...reference,
-        localRef: definitionContainer.nameFromId(reference.localRef),
-        foreignRef: definitionContainer.nameFromId(reference.foreignRef),
+        localRef: safeNameFromId(reference.localRef),
+        foreignRef: safeNameFromId(reference.foreignRef),
       })),
     })) ?? [];

🏁 Script executed:

#!/bin/bash
# Search for error handling in nameFromId implementations
rg -A 5 "nameFromId.*\{" 

Length of output: 2762


Enforce Strict Error Handling for Name Resolution in diffModel

The current implementation directly calls definitionContainer.nameFromId when processing relation references. Although nameFromId already throws an error if it can’t resolve a given non-empty ID, it returns undefined if passed an undefined value. This silent failure for undefined IDs may lead to unintended behavior in the diff results. I recommend introducing a safe wrapper—such as a safeNameFromId helper—that ensures every call to resolve an ID returns a valid name (throwing an error otherwise). For example:

 export function diffModel(
   current: ModelConfig['model'],
   desired: ModelDefinitionInput,
   definitionContainer: ProjectDefinitionContainer,
   options?: ModelDiffOptions,
 ): ModelDiffOutput | undefined {
+  const safeNameFromId = (id: string | undefined): string => {
+    const name = definitionContainer.nameFromId(id);
+    if (!name) {
+      throw new Error(`Failed to resolve a valid name for ID: ${id}`);
+    }
+    return name;
+  };
+
   // Resolve relation references.
   const resolvedRelations =
     current.relations?.map((relation) => ({
       ...relation,
       references: relation.references.map((reference) => ({
         ...reference,
-        localRef: definitionContainer.nameFromId(reference.localRef),
-        foreignRef: definitionContainer.nameFromId(reference.foreignRef),
+        localRef: safeNameFromId(reference.localRef),
+        foreignRef: safeNameFromId(reference.foreignRef),
       })),
     })) ?? [];

This change will ensure that any unresolved or missing IDs (even those that might be undefined) are caught early and handled explicitly rather than failing silently.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function diffModel(
current: ModelConfig['model'],
desired: ModelDefinitionInput,
definitionContainer: ProjectDefinitionContainer,
options?: ModelDiffOptions,
): ModelDiffOutput | undefined {
// Resolve relation references.
const resolvedRelations =
current.relations?.map((relation) => ({
...relation,
references: relation.references.map((reference) => ({
...reference,
localRef: definitionContainer.nameFromId(reference.localRef),
foreignRef: definitionContainer.nameFromId(reference.foreignRef),
})),
})) ?? [];
const fieldDiffs = diffByKey(
current.fields,
desired.fields,
(f) => f.name,
options,
);
const relationDiffs = diffByKey(
resolvedRelations,
desired.relations ?? [],
(r) => r.name,
options,
);
const uniqueConstraintDiffs = diffByKey(
(current.uniqueConstraints ?? []).map((constraint) => ({
...constraint,
fields: constraint.fields.map((f) => ({
...f,
fieldRef: definitionContainer.nameFromId(f.fieldRef),
})),
})),
desired.uniqueConstraints ?? [],
getUniqueConstraintKey,
options,
);
const pkDiff = isEqual(
current.primaryKeyFieldRefs.map((ref) =>
definitionContainer.nameFromId(ref),
),
desired.primaryKeyFieldRefs,
)
? undefined
: desired.primaryKeyFieldRefs;
if (
fieldDiffs.length > 0 ||
relationDiffs.length > 0 ||
uniqueConstraintDiffs.length > 0 ||
pkDiff
) {
return {
fields: fieldDiffs,
relations: relationDiffs,
uniqueConstraints: uniqueConstraintDiffs,
primaryKeyFieldRefs: pkDiff,
};
}
return undefined;
}
export function diffModel(
current: ModelConfig['model'],
desired: ModelDefinitionInput,
definitionContainer: ProjectDefinitionContainer,
options?: ModelDiffOptions,
): ModelDiffOutput | undefined {
const safeNameFromId = (id: string | undefined): string => {
const name = definitionContainer.nameFromId(id);
if (!name) {
throw new Error(`Failed to resolve a valid name for ID: ${id}`);
}
return name;
};
// Resolve relation references.
const resolvedRelations =
current.relations?.map((relation) => ({
...relation,
references: relation.references.map((reference) => ({
...reference,
localRef: safeNameFromId(reference.localRef),
foreignRef: safeNameFromId(reference.foreignRef),
})),
})) ?? [];
const fieldDiffs = diffByKey(
current.fields,
desired.fields,
(f) => f.name,
options,
);
const relationDiffs = diffByKey(
resolvedRelations,
desired.relations ?? [],
(r) => r.name,
options,
);
const uniqueConstraintDiffs = diffByKey(
(current.uniqueConstraints ?? []).map((constraint) => ({
...constraint,
fields: constraint.fields.map((f) => ({
...f,
fieldRef: definitionContainer.nameFromId(f.fieldRef),
})),
})),
desired.uniqueConstraints ?? [],
getUniqueConstraintKey,
options,
);
const pkDiff = isEqual(
current.primaryKeyFieldRefs.map((ref) =>
definitionContainer.nameFromId(ref),
),
desired.primaryKeyFieldRefs,
)
? undefined
: desired.primaryKeyFieldRefs;
if (
fieldDiffs.length > 0 ||
relationDiffs.length > 0 ||
uniqueConstraintDiffs.length > 0 ||
pkDiff
) {
return {
fields: fieldDiffs,
relations: relationDiffs,
uniqueConstraints: uniqueConstraintDiffs,
primaryKeyFieldRefs: pkDiff,
};
}
return undefined;
}

@kingston kingston merged commit 3b83763 into main Feb 13, 2025
5 checks passed
@kingston kingston deleted the kingston/eng-469-when-syncing-and-saving-recently-modified-error-shows-up branch February 13, 2025 10:11
@github-actions github-actions bot mentioned this pull request Feb 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants