Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7d9650d
feat: Add session error link to catch scenarios where we have an inva…
kingston Jul 11, 2025
1eeadb5
Export root auth apollo generator
kingston Jul 11, 2025
c46c8ad
Make sure we use NodeNext/Node16 for ts libraries
kingston Jul 11, 2025
6aba93b
Remove unnecessary module directive
kingston Jul 11, 2025
85156da
Fix one last reference
kingston Jul 11, 2025
ac10e03
Add exception for filename casing for $ and - router paths
kingston Jul 11, 2025
9232659
Incorporate updated storage plugin adapters
kingston Jul 11, 2025
1b25d83
Add json deep clone function
kingston Jul 12, 2025
d5db106
Refactor model form to fix bug with field creation
kingston Jul 12, 2025
ba86683
Small refactor of model field form
kingston Jul 12, 2025
3855f2b
Support broader set of file model fields
kingston Jul 12, 2025
9ad05ee
Ignore text encoding identifier case eslint errors
kingston Jul 12, 2025
9b889b9
Implement improved storage module
kingston Jul 12, 2025
f081e8b
Fix up react generators too
kingston Jul 12, 2025
ed6e3e9
Update storage inputs
kingston Jul 12, 2025
58434cb
Add changeset
kingston Jul 12, 2025
b260567
Bump todo task
kingston Jul 12, 2025
65776ad
Collocate file categories with modules
kingston Jul 12, 2025
c9b80a8
Update UI of storage plugin
kingston Jul 12, 2025
7ca3d37
Merge branch 'main' into kingston/eng-791-refactor-storage-module-bac…
kingston Jul 12, 2025
9e6211d
Fix knip errors
kingston Jul 12, 2025
2764d7b
Fix knip errors
kingston Jul 12, 2025
840c45f
Fix knip one final time
kingston Jul 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/dull-words-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'@baseplate-dev/plugin-storage': patch
---

Refactor storage plugin file category system to use registry-based pattern

This change modernizes the file category system by moving from a centralized configuration array to a modular registry-based pattern with individual category files. Key improvements include:

**New Architecture:**

- Individual category files for better modularity and maintainability
- `createFileCategory` utility with FileSize and MimeTypes helpers
- Registry pattern with `FILE_CATEGORY_REGISTRY` for type-safe category lookup
- GraphQL enum type for file categories with strict validation

**Enhanced Features:**

- If-None-Match header support for S3 uploads to prevent file overwrites
- Improved authorization patterns with separate upload/read permissions
- Better error messages and validation feedback
- Type-safe category name validation using CONSTANT_CASE convention

**Breaking Changes:**

- File categories are now imported from individual files instead of centralized array
- GraphQL schema now uses enum type instead of string for category field
- Authorization interface updated with separate upload/read functions

**Migration:**

- Existing file categories are preserved with same functionality
- Services updated to use new registry lookup functions
- Database schema remains compatible
5 changes: 5 additions & 0 deletions .changeset/plenty-mails-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@baseplate-dev/utils': patch
---

Add JSON deep clone function
6 changes: 6 additions & 0 deletions .changeset/short-vans-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@baseplate-dev/react-generators': patch
'@baseplate-dev/tools': patch
---

Add exception for filename casing for $ and - router paths
10 changes: 10 additions & 0 deletions packages/core-generators/src/generators/node/eslint/react-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ export const REACT_ESLINT_RULES = tsCodeFragment(
rules: {
// We use replace since it is not supported by ES2020
'unicorn/prefer-string-replace-all': 'off',
// Support kebab case with - prefix to support ignored files in routes and $ prefix for Tanstack camelCase files
'unicorn/filename-case': [
'error',
{
cases: {
kebabCase: true,
},
ignore: [String.raw\`^-[a-z0-9\\-\\.]+$\`, String.raw\`^\\$[a-zA-Z0-9\\.]+$\`],
},
],
},
},
`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ export default tsEslint.config(

// Can be too strict if you prefer to have shorter cases for negated conditions
'unicorn/no-negated-condition': 'off',

// Allow usage of utf-8 text encoding since it's consistent with the WHATWG spec
// and autofixing can cause unexpected changes (https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1926)
'unicorn/text-encoding-identifier-case': 'off',
},
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function runPluginMigrations(
try {
pluginDefinition.config = migration.migrate(
pluginDefinition.config,
draft,
);
} catch (error) {
throw new Error(
Expand Down
11 changes: 10 additions & 1 deletion packages/project-builder-lib/src/plugins/spec/config-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@ import { createPluginSpec } from './types.js';
export interface PluginConfigMigration {
version: number;
name: string;
migrate: (config: unknown) => unknown;
/**
* The function to migrate the plugin config.
*
* TODO: We should figure out a better way of mutating the project definition
*
* @param config - The plugin config to migrate
* @param projectDefinition - The project definition (it is mutable but be careful)
* @returns The migrated plugin config
*/
migrate: (config: unknown, projectDefinition: unknown) => unknown;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import type { ModelConfigInput } from '@baseplate-dev/project-builder-lib';
import type React from 'react';

import { useBlockBeforeContinue } from '@baseplate-dev/project-builder-lib/web';
import {
createModelBaseSchema,
FeatureUtils,
modelEntityType,
modelScalarFieldEntityType,
} from '@baseplate-dev/project-builder-lib';
import {
useBlockBeforeContinue,
useDefinitionSchema,
useProjectDefinition,
} from '@baseplate-dev/project-builder-lib/web';
import {
Button,
Dialog,
Expand All @@ -13,8 +24,12 @@ import {
DialogTrigger,
useControlledState,
} from '@baseplate-dev/ui-components';
import { zodResolver } from '@hookform/resolvers/zod';
import { useNavigate } from '@tanstack/react-router';
import { sortBy } from 'es-toolkit';
import { useMemo } from 'react';
import { useForm } from 'react-hook-form';

import { useModelForm } from '../-hooks/use-model-form.js';
import { ModelInfoForm } from '../edit.$key/-components/model-info-form.js';

interface NewModelDialogProps {
Expand All @@ -23,23 +38,109 @@ interface NewModelDialogProps {
onOpenChange?: (open: boolean) => void;
}

function createNewModel(): ModelConfigInput {
const idFieldId = modelScalarFieldEntityType.generateNewId();
return {
id: modelEntityType.generateNewId(),
name: '',
featureRef: '',
service: {
create: { enabled: false },
update: { enabled: false },
delete: { enabled: false },
transformers: [],
},
model: {
primaryKeyFieldRefs: [idFieldId],
fields: [
{
id: idFieldId,
name: 'id',
type: 'uuid',
isOptional: false,
options: {
default: '',
genUuid: true,
},
},
],
},
};
}

export function NewModelDialog({
children,
open,
onOpenChange,
}: NewModelDialogProps): React.JSX.Element {
}: NewModelDialogProps): React.ReactElement {
const [isOpen, setIsOpen] = useControlledState(open, onOpenChange, false);
const {
onSubmit,
form: { control },
} = useModelForm({
isCreate: true,
modelKey: undefined,
onSubmitSuccess: () => {
setIsOpen(false);
},
const { definition, saveDefinitionWithFeedback } = useProjectDefinition();
// memoize it to keep the same key when resetting
const baseModelSchema = useDefinitionSchema(createModelBaseSchema);

const defaultValues = useMemo(() => createNewModel(), []);
const navigate = useNavigate();

const { handleSubmit, reset, setError, control } = useForm({
resolver: zodResolver(baseModelSchema),
defaultValues,
});

const onSubmit = useMemo(
() =>
handleSubmit((data) => {
// check for models with the same name
const existingModel = definition.models.find(
(m) => m.name.toLowerCase() === data.name.toLowerCase(),
);
if (existingModel) {
setError('name', {
message: `Model with name ${data.name} already exists.`,
});
return;
}

return saveDefinitionWithFeedback(
(draftConfig) => {
// create feature if a new feature exists
const updatedModel = { ...data };
updatedModel.featureRef =
FeatureUtils.ensureFeatureByNameRecursively(
draftConfig,
updatedModel.featureRef,
);
draftConfig.models = sortBy(
[
...draftConfig.models.filter((m) => m.id !== updatedModel.id),
updatedModel,
],
[(m) => m.name],
);
},
{
successMessage: 'Successfully created model!',
onSuccess: () => {
navigate({
to: '/data/models/edit/$key',
params: { key: modelEntityType.keyFromId(data.id) },
});
reset(createNewModel());
setIsOpen(false);
},
},
);
}),
[
reset,
setError,
handleSubmit,
saveDefinitionWithFeedback,
navigate,
definition,
setIsOpen,
],
);

const blockBeforeContinue = useBlockBeforeContinue();

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import type React from 'react';
import type { UseFormGetValues, UseFormWatch } from 'react-hook-form';
import type { StoreApi } from 'zustand';

import { ModelUtils } from '@baseplate-dev/project-builder-lib';
import { useProjectDefinition } from '@baseplate-dev/project-builder-lib/web';
import { jsonDeepClone } from '@baseplate-dev/utils';
import { createContext, useContext, useEffect, useMemo } from 'react';
import { createStore, useStore } from 'zustand';

Expand All @@ -24,41 +23,40 @@ const EditedModelContext = createContext<
>(undefined);

export function EditedModelContextProvider({
originalModel,
children,
watch,
initialModel,
getValues,
}: {
originalModel: ModelConfig;
children: React.ReactNode;
watch: UseFormWatch<ModelConfigInput>;
getValues: UseFormGetValues<ModelConfigInput>;
initialModel: ModelConfigInput;
}): React.JSX.Element {
const { definition } = useProjectDefinition();
const existingModel = ModelUtils.byIdOrThrow(definition, initialModel.id);
const store = useMemo(
() =>
createStore<ModelConfigStore>((set) => ({
model: {
...existingModel,
...initialModel,
...originalModel,
...getValues(),
},
setModel: (model) => {
set({
model: {
...existingModel,
...originalModel,
...model,
},
});
},
getValues,
})),
[initialModel, getValues, existingModel],
[originalModel, getValues],
);

useEffect(() => {
const { unsubscribe } = watch((data) => {
store.getState().setModel(data as ModelConfig);
// We need to clone the data since React hook form data store is not immutable
store.getState().setModel(jsonDeepClone(data as ModelConfigInput));
});
return unsubscribe;
}, [watch, store]);
Expand Down
Loading