├── .depcheckrc ├── .editorconfig ├── .eslintignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ ├── build │ │ └── action.yml │ ├── check │ │ └── action.yml │ ├── job-publish │ │ └── action.yml │ ├── job-verify │ │ └── action.yml │ ├── publish-npm │ │ └── action.yml │ ├── setup │ │ └── action.yml │ └── test │ │ └── action.yml └── workflows │ ├── core.yml │ ├── create.yml │ ├── crypto.yml │ ├── design-mui.yml │ ├── express.yml │ ├── koa.yml │ ├── node.yml │ ├── playwright.yml │ ├── push.yml │ ├── react-rest.yml │ ├── react-ui.yml │ ├── react.yml │ ├── release.yml │ ├── rest-client-jsonapi.yml │ └── rest-client.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── hatchify.iml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── .nvmrc ├── .prettierignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── NAMESPACE-SCHEMAS.md ├── api-documentation.md ├── attachments │ ├── FiltersClosed.png │ ├── FiltersOpen.png │ ├── List.png │ ├── ListWithPagination.png │ ├── add-postgres.png │ ├── create-database.png │ ├── customNoRecords.png │ ├── data-grid-example.png │ ├── dateonly-column.png │ ├── datetime-column.png │ ├── defaultNoRecords.png │ ├── enum-grid.png │ ├── frontend-todo-list.png │ ├── integer-column.png │ ├── number-column.png │ ├── reactTs.gif │ ├── stringNullFilter.png │ └── text-grid.png ├── code-examples.md ├── core │ ├── PartialSchema.md │ ├── README.md │ ├── attribute-types │ │ ├── README.md │ │ ├── boolean.md │ │ ├── dateonly.md │ │ ├── datetime.md │ │ ├── enum.md │ │ ├── integer.md │ │ ├── number.md │ │ ├── string.md │ │ ├── text.md │ │ └── uuid.md │ └── relationship-types │ │ ├── README.md │ │ ├── belongs-to.md │ │ ├── has-many-through.md │ │ ├── has-many.md │ │ └── has-one.md ├── express │ ├── README.md │ ├── hatchedExpress.everything.md │ ├── hatchedExpress.middleware.md │ ├── hatchedExpress.model.md │ ├── hatchedExpress.parse.md │ └── hatchedExpress.serialize.md ├── guides │ ├── adding-checkboxes-to-the-list.md │ ├── adding-custom-endpoints.md │ ├── adding-form-like-behavior.md │ ├── adding-request-authorization.md │ ├── application-data-validation.md │ ├── customizing-your-list.md │ ├── production-custom-usage.md │ └── using-postgres-db.md ├── jsonapi │ ├── README.md │ ├── creating.md │ ├── deleting.md │ ├── reading │ │ ├── README.md │ │ ├── filtering │ │ │ ├── $eq.md │ │ │ ├── $gt.md │ │ │ ├── $gte.md │ │ │ ├── $ilike.md │ │ │ ├── $in.md │ │ │ ├── $like.md │ │ │ ├── $lt.md │ │ │ ├── $lte.md │ │ │ ├── $ne.md │ │ │ ├── $nin.md │ │ │ ├── README.md │ │ │ └── no-operator.md │ │ ├── paginating │ │ │ └── README.md │ │ ├── relationships │ │ │ └── README.md │ │ ├── sorting │ │ │ └── README.md │ │ └── sparse-fields │ │ │ └── README.md │ └── updating.md ├── koa │ ├── README.md │ ├── hatchedKoa.everything.md │ ├── hatchedKoa.middleware.md │ ├── hatchedKoa.model.md │ ├── hatchedKoa.parse.md │ └── hatchedKoa.serialize.md ├── react-jsonapi │ └── README.md ├── react │ ├── README.md │ ├── hatchedReact.Navigation.md │ ├── hatchedReact.components.everything.md │ ├── hatchedReact.components.md │ ├── hatchedReact.components[schemaName].DataGrid.Column.md │ ├── hatchedReact.components[schemaName].DataGrid.Empty.md │ ├── hatchedReact.components[schemaName].DataGrid.md │ ├── hatchedReact.components[schemaName].Filters.md │ ├── hatchedReact.components[schemaName].List.md │ ├── hatchedReact.components[schemaName].Pagination.md │ ├── hatchedReact.model.md │ ├── hatchedReact.state.md │ └── types.md └── subpackages.md ├── e2e ├── App.txt ├── main.txt ├── playwright.config.ts ├── schemas.txt └── tests │ ├── getting-started.spec.ts │ ├── grid-demo.spec.ts │ ├── react-rest-jsonapi.spec.ts │ └── react-rest.spec.ts ├── example ├── getting-started │ ├── .env │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── backend │ │ └── index.ts │ ├── frontend │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── index.html │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── schemas.ts │ ├── tsconfig.backend.json │ ├── tsconfig.json │ └── vite.config.ts ├── grid-demo │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── backend │ │ ├── index.ts │ │ └── util.ts │ ├── docker-compose.yaml │ ├── frontend │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── components │ │ │ └── DocumentTable.tsx │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── index.html │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── schemas.ts │ ├── tsconfig.backend.json │ ├── tsconfig.json │ └── vite.config.ts ├── namespaces │ ├── .env │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── backend │ │ └── index.ts │ ├── frontend │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── index.html │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── schemas.ts │ ├── tsconfig.backend.json │ ├── tsconfig.json │ └── vite.config.ts ├── react-rest-hatchify-backend │ ├── .eslintrc.json │ ├── .gitignore │ ├── backend │ │ └── index.ts │ ├── frontend │ │ ├── App.css │ │ ├── App.tsx │ │ ├── WithoutHooks.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── index.html │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── schemas.ts │ ├── tsconfig.backend.json │ ├── tsconfig.json │ └── vite.config.ts └── react-rest │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── mockServiceWorker.js │ └── vite.svg │ ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ ├── mocks │ │ └── browser.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── nx.json ├── package-lock.json ├── package.json ├── packages ├── core │ ├── .depcheckrc │ ├── .prettierignore │ ├── README.md │ ├── jest.config.json │ ├── package.json │ ├── src │ │ ├── assembler │ │ │ ├── ensureDefaultPrimaryAttribute.spec.ts │ │ │ ├── ensureDefaultPrimaryAttribute.ts │ │ │ ├── ensureDefaultReadOnlyAttribute.spec.ts │ │ │ ├── ensureDefaultReadOnlyAttribute.ts │ │ │ ├── finalizeRelationships.spec.ts │ │ │ ├── finalizeRelationships.ts │ │ │ ├── finalizeSchema.spec.ts │ │ │ ├── finalizeSchema.ts │ │ │ ├── getDefaultPrimaryAttribute.spec.ts │ │ │ ├── getDefaultPrimaryAttribute.ts │ │ │ ├── index.spec.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── core.ts │ │ ├── dataTypes │ │ │ ├── boolean │ │ │ │ ├── coerce.spec.ts │ │ │ │ ├── coerce.ts │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── dateonly │ │ │ │ ├── coerce.spec.ts │ │ │ │ ├── coerce.ts │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isISO8601DateString.spec.ts │ │ │ │ ├── isISO8601DateString.ts │ │ │ │ ├── types.ts │ │ │ │ ├── validateStep.spec.ts │ │ │ │ └── validateStep.ts │ │ │ ├── datetime │ │ │ │ ├── coerce.spec.ts │ │ │ │ ├── coerce.ts │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isISO8601DatetimeString.spec.ts │ │ │ │ ├── isISO8601DatetimeString.ts │ │ │ │ ├── types.ts │ │ │ │ ├── validateStep.spec.ts │ │ │ │ └── validateStep.ts │ │ │ ├── enumerate │ │ │ │ ├── coerce.spec.ts │ │ │ │ ├── coerce.ts │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── validateValues.spec.ts │ │ │ │ └── validateValues.ts │ │ │ ├── index.ts │ │ │ ├── integer │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── number │ │ │ │ ├── coerce.spec.ts │ │ │ │ ├── coerce.ts │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── validateStep.spec.ts │ │ │ │ └── validateStep.ts │ │ │ ├── string │ │ │ │ ├── coerce.spec.ts │ │ │ │ ├── coerce.ts │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── text │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── uuid │ │ │ │ ├── constants.ts │ │ │ │ ├── finalizeControl.spec.ts │ │ │ │ ├── finalizeControl.ts │ │ │ │ ├── finalizeOrm.spec.ts │ │ │ │ ├── finalizeOrm.ts │ │ │ │ ├── getFinalize.spec.ts │ │ │ │ ├── getFinalize.ts │ │ │ │ ├── getPartialControl.spec.ts │ │ │ │ ├── getPartialControl.ts │ │ │ │ ├── getPartialOrm.spec.ts │ │ │ │ ├── getPartialOrm.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ ├── relationships │ │ │ ├── belongsTo │ │ │ │ ├── finalize.spec.ts │ │ │ │ ├── finalize.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── hasMany │ │ │ │ ├── finalize.spec.ts │ │ │ │ ├── finalize.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── hasManyThrough │ │ │ │ ├── buildThrough.spec.ts │ │ │ │ ├── buildThrough.ts │ │ │ │ ├── finalize.spec.ts │ │ │ │ ├── finalize.ts │ │ │ │ └── types.ts │ │ │ ├── hasOne │ │ │ │ ├── finalize.spec.ts │ │ │ │ ├── finalize.ts │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── getForeignKeyAttribute.spec.ts │ │ │ │ └── getForeignKeyAttribute.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ └── util │ │ │ ├── camelCaseToPascalCase.spec.ts │ │ │ ├── camelCaseToPascalCase.ts │ │ │ ├── camelCaseToTitleCase.spec.ts │ │ │ ├── camelCaseToTitleCase.ts │ │ │ ├── getEndpoint.spec.ts │ │ │ ├── getEndpoint.ts │ │ │ ├── getSchemaKey.spec.ts │ │ │ ├── getSchemaKey.ts │ │ │ ├── mergeSchemaUI.spec.ts │ │ │ ├── mergeSchemaUI.ts │ │ │ ├── pascalCaseToCamelCase.spec.ts │ │ │ ├── pascalCaseToCamelCase.ts │ │ │ ├── pascalCaseToKebabCase.spec.ts │ │ │ ├── pascalCaseToKebabCase.ts │ │ │ ├── pluralize.spec.ts │ │ │ ├── pluralize.ts │ │ │ ├── singularize.spec.ts │ │ │ ├── singularize.ts │ │ │ └── uuidv4.ts │ └── tsconfig.json ├── create │ ├── .depcheckrc │ ├── README.md │ ├── build.config.ts │ ├── index.js │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── util.ts │ ├── template-express │ │ ├── _gitignore │ │ ├── backend │ │ │ └── index.ts │ │ └── schemas.ts │ ├── template-koa │ │ ├── _gitignore │ │ ├── backend │ │ │ └── index.ts │ │ └── schemas.ts │ ├── template-react │ │ ├── App.tsx │ │ ├── index.css │ │ └── main.tsx │ └── tsconfig.json ├── crypto │ ├── .depcheckrc │ ├── README.md │ ├── index.browser.js │ ├── index.browser.spec.js │ ├── index.d.ts │ ├── index.js │ ├── index.spec.js │ ├── jest.config.json │ └── package.json ├── design-mui │ ├── .depcheckrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── assets │ │ │ ├── Robochicken.tsx │ │ │ └── index.ts │ │ ├── components │ │ │ ├── MuiAutocomplete │ │ │ │ ├── MuiAutocomplete.test.tsx │ │ │ │ ├── MuiAutocomplete.tsx │ │ │ │ └── index.tsx │ │ │ ├── MuiDataGrid │ │ │ │ ├── MuiDataGrid.tsx │ │ │ │ └── index.ts │ │ │ ├── MuiEverything │ │ │ │ ├── MuiEverything.test.tsx │ │ │ │ ├── MuiEverything.tsx │ │ │ │ └── index.ts │ │ │ ├── MuiFilters │ │ │ │ ├── MuiFilters.test.tsx │ │ │ │ ├── MuiFilters.tsx │ │ │ │ ├── components │ │ │ │ │ ├── MuiFilterRows.test.tsx │ │ │ │ │ ├── MuiFilterRows.tsx │ │ │ │ │ └── inputs │ │ │ │ │ │ ├── ColumnSelect.test.tsx │ │ │ │ │ │ ├── ColumnSelect.tsx │ │ │ │ │ │ ├── DateInput.test.tsx │ │ │ │ │ │ ├── DateInput.tsx │ │ │ │ │ │ ├── EnumInput.test.tsx │ │ │ │ │ │ ├── EnumInput.tsx │ │ │ │ │ │ ├── NumberInput.tsx │ │ │ │ │ │ ├── OperatorSelect.test.tsx │ │ │ │ │ │ ├── OperatorSelect.tsx │ │ │ │ │ │ ├── StringInput.test.tsx │ │ │ │ │ │ ├── StringInput.tsx │ │ │ │ │ │ └── ValueInput.tsx │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── utils.test.tsx │ │ │ │ │ └── utils.tsx │ │ │ ├── MuiLayout │ │ │ │ ├── MuiLayout.test.tsx │ │ │ │ ├── MuiLayout.tsx │ │ │ │ └── index.ts │ │ │ ├── MuiList │ │ │ │ ├── MuiList.test.tsx │ │ │ │ ├── MuiList.tsx │ │ │ │ ├── components │ │ │ │ │ ├── MuiBody.tsx │ │ │ │ │ ├── MuiBodySkeleton.tsx │ │ │ │ │ ├── MuiHeaders │ │ │ │ │ │ ├── MuiHeaders.tsx │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── Sort.tsx │ │ │ │ │ │ │ ├── Sortable.tsx │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── MuiNavigation │ │ │ │ ├── MuiNavigation.tsx │ │ │ │ └── index.ts │ │ │ ├── MuiNoSchemas │ │ │ │ ├── MuiNoSchemas.tsx │ │ │ │ └── index.ts │ │ │ ├── MuiPagination │ │ │ │ ├── MuiPagination.test.tsx │ │ │ │ ├── MuiPagination.tsx │ │ │ │ └── index.ts │ │ │ ├── MuiProvider │ │ │ │ ├── DefaultDisplayComponents │ │ │ │ │ ├── DefaultDisplayComponents.test.tsx │ │ │ │ │ ├── DefaultDisplayComponents.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── DefaultFieldComponents │ │ │ │ │ ├── DefaultFieldComponents.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── MuiProvider.test.tsx │ │ │ │ ├── MuiProvider.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── design-mui.ts │ │ └── services │ │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── express │ ├── .depcheckrc │ ├── .gitignore │ ├── .prettierignore │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.json │ ├── package.json │ ├── src │ │ ├── attributes.spec.ts │ │ ├── database.spec.ts │ │ ├── errorCodes.spec.ts │ │ ├── exports.ts │ │ ├── express.ts │ │ ├── jsonapi.spec.ts │ │ ├── jsonapiNamespace.spec.ts │ │ ├── middleware.spec.ts │ │ ├── middleware.ts │ │ ├── naming.spec.ts │ │ ├── queryStringFilters.spec.ts │ │ ├── relationships.spec.ts │ │ ├── relationshipsv2.spec.ts │ │ ├── schema.spec.ts │ │ └── testing │ │ │ └── utils.ts │ └── tsconfig.json ├── koa │ ├── .depcheckrc │ ├── .gitignore │ ├── .prettierignore │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.json │ ├── package.json │ ├── src │ │ ├── attributes.spec.ts │ │ ├── custom.spec.ts │ │ ├── database.spec.ts │ │ ├── errorCodes.spec.ts │ │ ├── exports.ts │ │ ├── hatchify-gsg-seed.ts │ │ ├── jsonapi.spec.ts │ │ ├── jsonapiNamespace.spec.ts │ │ ├── koa.ts │ │ ├── middleware.ts │ │ ├── naming.spec.ts │ │ ├── queryStringFilters.spec.ts │ │ ├── relationships.spec.ts │ │ ├── relationshipsv2.spec.ts │ │ ├── schema.spec.ts │ │ └── testing │ │ │ └── utils.ts │ ├── tsconfig.json │ └── typedoc.json ├── node │ ├── .depcheckrc │ ├── .gitignore │ ├── .prettierignore │ ├── README.md │ ├── jest.config.json │ ├── package.json │ ├── src │ │ ├── error │ │ │ ├── constants.ts │ │ │ ├── handlers │ │ │ │ ├── errorResponseHandler.ts │ │ │ │ ├── hatchifyErrorHandler.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── types │ │ │ │ ├── ConflictError.ts │ │ │ │ ├── HatchifyError.ts │ │ │ │ ├── NotFoundError.ts │ │ │ │ ├── RelationshipPathError.ts │ │ │ │ ├── UnexpectedValueError.ts │ │ │ │ ├── ValueRequiredError.ts │ │ │ │ └── index.ts │ │ ├── everything.ts │ │ ├── exports.ts │ │ ├── internals.spec.ts │ │ ├── jest.d.ts │ │ ├── jest.setup.ts │ │ ├── middleware.spec.ts │ │ ├── middleware.ts │ │ ├── node.spec.ts │ │ ├── node.ts │ │ ├── parse │ │ │ ├── IsValidInclude.spec.ts │ │ │ ├── IsValidInclude.ts │ │ │ ├── body.ts │ │ │ ├── builder.spec.ts │ │ │ ├── builder.ts │ │ │ ├── getColumnName.spec.ts │ │ │ ├── getColumnName.ts │ │ │ ├── handlePostgresUuid.ts │ │ │ ├── handleSqliteDateNestedColumns.ts │ │ │ ├── handleSqliteLike.spec.ts │ │ │ ├── handleSqliteLike.ts │ │ │ ├── handleWhere.ts │ │ │ ├── index.spec.ts │ │ │ ├── index.ts │ │ │ ├── indexNamespace.spec.ts │ │ │ ├── isPathIncluded.spec.ts │ │ │ ├── isPathIncluded.ts │ │ │ ├── isValidAttribute.spec.ts │ │ │ ├── isValidAttribute.ts │ │ │ ├── validator.ts │ │ │ └── walk.ts │ │ ├── schema.ts │ │ ├── sequelize │ │ │ ├── convertHatchifyModels.spec.ts │ │ │ ├── convertHatchifyModels.ts │ │ │ ├── createSequelizeInstance.ts │ │ │ ├── customTypes │ │ │ │ ├── CustomDecimal.ts │ │ │ │ └── CustomInteger.ts │ │ │ ├── getSequelizeSchemaName.spec.ts │ │ │ ├── getSequelizeSchemaName.ts │ │ │ ├── index.ts │ │ │ ├── toSequelize.spec.ts │ │ │ └── toSequelize.ts │ │ ├── serialize.ts │ │ └── types.ts │ └── tsconfig.json ├── react-jsonapi │ ├── .depcheckrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── react-jsonapi.test.ts │ │ └── react-jsonapi.tsx │ └── tsconfig.json ├── react-rest │ ├── .depcheckrc │ ├── README.md │ ├── doc │ │ └── attachments │ │ │ ├── relationships.png │ │ │ ├── todo-app-with-relationships.png │ │ │ ├── todo-app.png │ │ │ ├── ts.gif │ │ │ └── ui.gif │ ├── package.json │ ├── src │ │ ├── react-rest.ts │ │ ├── services │ │ │ ├── hatchifyReactRest │ │ │ │ ├── hatchifyReactRest.test.ts │ │ │ │ ├── hatchifyReactRest.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── useAll │ │ │ │ ├── index.ts │ │ │ │ ├── useAll.test.ts │ │ │ │ └── useAll.ts │ │ │ ├── useCreateOne │ │ │ │ ├── index.ts │ │ │ │ ├── useCreateOne.test.ts │ │ │ │ └── useCreateOne.ts │ │ │ ├── useDeleteOne │ │ │ │ ├── index.ts │ │ │ │ ├── useDeleteOne.test.ts │ │ │ │ └── useDeleteOne.ts │ │ │ ├── useOne │ │ │ │ ├── index.ts │ │ │ │ ├── useOne.test.ts │ │ │ │ └── useOne.ts │ │ │ └── useUpdateOne │ │ │ │ ├── index.ts │ │ │ │ ├── useUpdateOne.test.ts │ │ │ │ └── useUpdateOne.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── react-ui │ ├── .depcheckrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── HatchifyCollection │ │ │ │ └── index.ts │ │ │ ├── HatchifyColumn │ │ │ │ ├── HatchifyColumn.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyDataGrid │ │ │ │ ├── HatchifyDataGrid.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyEmpty │ │ │ │ ├── HatchifyEmpty.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyEverything │ │ │ │ ├── HatchifyEverything.tsx │ │ │ │ ├── components │ │ │ │ │ ├── NoSchemas.tsx │ │ │ │ │ ├── WithSchemas.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── HatchifyFilters │ │ │ │ ├── HatchifyFilters.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyList │ │ │ │ ├── HatchifyList.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyNavigation │ │ │ │ ├── HatchifyNavigation.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyNoSchemas │ │ │ │ ├── HatchifyNoSchemas.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyPagination │ │ │ │ ├── HatchifyPagination.tsx │ │ │ │ └── index.ts │ │ │ ├── HatchifyPresentationProvider │ │ │ │ ├── DefaultDisplayComponents │ │ │ │ │ ├── DefaultDisplayComponents.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── DefaultFieldComponents │ │ │ │ │ ├── DefaultFieldComponents.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── HatchifyPresentationProvider.tsx │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── hatchifyReact │ │ │ ├── hatchifyReact.test.ts │ │ │ ├── hatchifyReact.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useCompoundComponents │ │ │ │ ├── helpers │ │ │ │ │ ├── getColumn.test.tsx │ │ │ │ │ ├── getColumn.tsx │ │ │ │ │ ├── getColumns.test.ts │ │ │ │ │ ├── getColumns.ts │ │ │ │ │ ├── getColumnsFromSchema.test.ts │ │ │ │ │ ├── getColumnsFromSchema.ts │ │ │ │ │ ├── getDefaultDataRender.test.ts │ │ │ │ │ ├── getDefaultDataRender.tsx │ │ │ │ │ ├── getEmptyList.test.tsx │ │ │ │ │ ├── getEmptyList.tsx │ │ │ │ │ └── index.ts │ │ │ │ └── useCompoundComponents.tsx │ │ │ ├── useDataGridState.test.ts │ │ │ ├── useDataGridState.ts │ │ │ ├── useFilter.test.ts │ │ │ ├── useFilter.ts │ │ │ ├── usePage.test.ts │ │ │ ├── usePage.ts │ │ │ ├── useSelected.test.ts │ │ │ ├── useSelected.ts │ │ │ ├── useSort.test.ts │ │ │ └── useSort.ts │ │ ├── presentation │ │ │ ├── index.ts │ │ │ └── interfaces.ts │ │ └── react-ui.ts │ ├── tsconfig.json │ └── vite.config.ts ├── react │ ├── .depcheckrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── react.test.ts │ │ └── react.tsx │ └── tsconfig.json ├── rest-client-jsonapi │ ├── .depcheckrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── mocks │ │ │ ├── handlers.ts │ │ │ └── server.ts │ │ ├── rest-client-jsonapi.ts │ │ ├── services │ │ │ ├── createOne │ │ │ │ ├── createOne.test.ts │ │ │ │ ├── createOne.ts │ │ │ │ └── index.ts │ │ │ ├── deleteOne │ │ │ │ ├── deleteOne.test.ts │ │ │ │ ├── deleteOne.ts │ │ │ │ └── index.ts │ │ │ ├── findAll │ │ │ │ ├── findAll.test.ts │ │ │ │ ├── findAll.ts │ │ │ │ └── index.ts │ │ │ ├── findOne │ │ │ │ ├── findOne.test.ts │ │ │ │ ├── findOne.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── jsonapi │ │ │ │ ├── index.ts │ │ │ │ ├── jsonapi.test.ts │ │ │ │ └── jsonapi.ts │ │ │ ├── services.test.ts │ │ │ ├── updateOne │ │ │ │ ├── index.ts │ │ │ │ ├── updateOne.test.ts │ │ │ │ └── updateOne.ts │ │ │ └── utils │ │ │ │ ├── fetch │ │ │ │ ├── fetch.test.ts │ │ │ │ ├── fetch.ts │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── query │ │ │ │ ├── index.ts │ │ │ │ ├── query.test.ts │ │ │ │ └── query.ts │ │ │ │ └── resources │ │ │ │ ├── index.ts │ │ │ │ ├── resources.test.ts │ │ │ │ └── resources.ts │ │ ├── setupTests.ts │ │ └── vite-env.d.ts │ └── tsconfig.json └── rest-client │ ├── .depcheckrc │ ├── README.md │ ├── package.json │ ├── src │ ├── rest-client.ts │ └── services │ │ ├── index.ts │ │ ├── mocks │ │ └── testData.ts │ │ ├── promise │ │ ├── createOne │ │ │ ├── createOne.test.ts │ │ │ ├── createOne.ts │ │ │ └── index.ts │ │ ├── deleteOne │ │ │ ├── deleteOne.test.ts │ │ │ ├── deleteOne.ts │ │ │ └── index.ts │ │ ├── findAll │ │ │ ├── findAll.test.ts │ │ │ ├── findAll.ts │ │ │ └── index.ts │ │ ├── findOne │ │ │ ├── findOne.test.ts │ │ │ ├── findOne.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── updateOne │ │ │ ├── index.ts │ │ │ ├── updateOne.test.ts │ │ │ └── updateOne.ts │ │ ├── store │ │ ├── index.ts │ │ ├── store.test.ts │ │ └── store.ts │ │ ├── subscribe │ │ ├── index.ts │ │ ├── subscribeToAll │ │ │ ├── index.ts │ │ │ ├── subscribeToAll.test.ts │ │ │ └── subscribeToAll.ts │ │ └── subscribeToOne │ │ │ ├── index.ts │ │ │ ├── subscribeToOne.test.ts │ │ │ └── subscribeToOne.ts │ │ ├── types │ │ ├── data.ts │ │ ├── index.ts │ │ ├── meta.ts │ │ ├── query.ts │ │ ├── rest-client.ts │ │ ├── schema.ts │ │ ├── subscribe.ts │ │ └── utility.ts │ │ └── utils │ │ ├── index.ts │ │ ├── meta.test.ts │ │ ├── meta.ts │ │ ├── notify.ts │ │ ├── records.test.ts │ │ ├── records.ts │ │ ├── request.test.ts │ │ ├── request.ts │ │ ├── response.test.ts │ │ ├── response.ts │ │ ├── schema.test.ts │ │ ├── schema.ts │ │ ├── selector.test.ts │ │ ├── selector.ts │ │ ├── unflatten.test.ts │ │ └── unflatten.ts │ └── tsconfig.json ├── scripts ├── bump-packages.ts ├── check-packages.ts ├── get-changed-packages.ts ├── lib │ └── packages.ts ├── pree2e.sh └── updategs.sh └── tsconfig.json /.depcheckrc: -------------------------------------------------------------------------------- 1 | ignore-dirs: 2 | - /dist 3 | ignores: 4 | - "@types/*" 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*.ts] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | example 4 | playwright-report -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: fix 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Installed Packages** 24 | Output of the `npm ls` command in your project's root folder. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Screenshots** 33 | If applicable, add screenshots to help explain your problem. 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | inputs: 4 | package: 5 | description: "The package folder name." 6 | required: true 7 | 8 | runs: 9 | using: "composite" 10 | 11 | steps: 12 | - name: Build 13 | shell: bash 14 | run: npx nx build ${{ inputs.package }} 15 | -------------------------------------------------------------------------------- /.github/actions/check/action.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | inputs: 4 | package: 5 | description: "The package folder name." 6 | required: true 7 | 8 | runs: 9 | using: "composite" 10 | 11 | steps: 12 | - name: Typecheck 13 | shell: bash 14 | run: npx nx typecheck ${{ inputs.package }} 15 | 16 | - name: ESLint 17 | shell: bash 18 | run: npx nx lint ${{ inputs.package }} 19 | 20 | - name: depcheck 21 | shell: bash 22 | run: npx nx depcheck ${{ inputs.package }} 23 | -------------------------------------------------------------------------------- /.github/actions/job-verify/action.yml: -------------------------------------------------------------------------------- 1 | name: job-verify 2 | 3 | inputs: 4 | node-version: 5 | description: "The Node version." 6 | required: true 7 | 8 | runs: 9 | using: "composite" 10 | 11 | steps: 12 | - name: Setup 13 | uses: ./.github/actions/setup 14 | with: 15 | node-version: ${{ inputs.node-version }} 16 | 17 | - name: Check 18 | uses: ./.github/actions/check 19 | with: 20 | package: ${{ github.workflow }} 21 | 22 | - name: Test 23 | uses: ./.github/actions/test 24 | with: 25 | package: ${{ github.workflow }} 26 | 27 | - name: Build 28 | uses: ./.github/actions/build 29 | with: 30 | package: ${{ github.workflow }} 31 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: setup 2 | 3 | inputs: 4 | node-version: 5 | description: "The Node version." 6 | required: true 7 | 8 | runs: 9 | using: "composite" 10 | 11 | steps: 12 | - name: Setup Node.js ${{ inputs.node-version }} environment 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: ${{ inputs.node-version }} 16 | registry-url: "https://registry.npmjs.org/" 17 | 18 | - name: Install dependencies 19 | shell: bash 20 | run: npm ci 21 | -------------------------------------------------------------------------------- /.github/actions/test/action.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | inputs: 4 | package: 5 | description: "The package folder name." 6 | required: true 7 | 8 | runs: 9 | using: "composite" 10 | 11 | steps: 12 | - name: Test 13 | shell: bash 14 | run: npx nx test ${{ inputs.package }} 15 | env: 16 | DB_URI: postgres://postgres:password@localhost:5432/postgres 17 | -------------------------------------------------------------------------------- /.github/workflows/core.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/core" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/create.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/create" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/crypto.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/crypto" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/design-mui.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/design-mui" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/express.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/express" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | services: 10 | postgres: 11 | image: postgres:alpine 12 | env: 13 | POSTGRES_USER: postgres 14 | POSTGRES_PASSWORD: password 15 | options: >- 16 | --health-cmd pg_isready 17 | --health-interval 10s 18 | --health-timeout 5s 19 | --health-retries 5 20 | ports: 21 | - 5432:5432 22 | 23 | strategy: 24 | matrix: 25 | node-version: [18.x, 20.x] 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Verify 34 | uses: ./.github/actions/job-verify 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | -------------------------------------------------------------------------------- /.github/workflows/koa.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/koa" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | services: 10 | postgres: 11 | image: postgres:alpine 12 | env: 13 | POSTGRES_USER: postgres 14 | POSTGRES_PASSWORD: password 15 | options: >- 16 | --health-cmd pg_isready 17 | --health-interval 10s 18 | --health-timeout 5s 19 | --health-retries 5 20 | ports: 21 | - 5432:5432 22 | 23 | strategy: 24 | matrix: 25 | node-version: [18.x, 20.x] 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | # - name: Postgres 34 | # uses: ./.github/actions/postgres 35 | 36 | - name: Verify 37 | uses: ./.github/actions/job-verify 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/node" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/react-rest.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/react-rest" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/react-ui.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/react-ui" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/react.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/react" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/release" 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | release: 9 | if: startsWith(github.ref, 'refs/tags/') 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Build Changelog 13 | id: build_changelog 14 | uses: mikepenz/release-changelog-builder-action@v3 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | - name: Create Release 19 | uses: mikepenz/action-gh-release@v0.2.0-a03 #softprops/action-gh-release 20 | with: 21 | body: ${{steps.build_changelog.outputs.changelog}} 22 | -------------------------------------------------------------------------------- /.github/workflows/rest-client-jsonapi.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/rest-client-jsonapi" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | services: 10 | postgres: 11 | image: postgres:alpine 12 | env: 13 | POSTGRES_USER: postgres 14 | POSTGRES_PASSWORD: password 15 | options: >- 16 | --health-cmd pg_isready 17 | --health-interval 10s 18 | --health-timeout 5s 19 | --health-retries 5 20 | ports: 21 | - 5432:5432 22 | 23 | strategy: 24 | matrix: 25 | node-version: [18.x, 20.x] 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Verify 34 | uses: ./.github/actions/job-verify 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | -------------------------------------------------------------------------------- /.github/workflows/rest-client.yml: -------------------------------------------------------------------------------- 1 | name: "@hatchifyjs/rest-client" 2 | 3 | on: push 4 | 5 | jobs: 6 | verify: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 20.x] 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Verify 20 | uses: ./.github/actions/job-verify 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # artifacts 2 | coverage 3 | node_modules 4 | /**/node_modules 5 | /packages/*/dist 6 | 7 | # misc 8 | .DS_Store 9 | Thumbs.db 10 | *.tsbuildinfo 11 | npm-debug.log* 12 | .obsidian 13 | .nx 14 | yalc.lock 15 | .yalc 16 | 17 | /e2e/hatchify-app 18 | /e2e/test-results/ 19 | /e2e/playwright-report/ 20 | /playwright/.cache/ 21 | /packages/create/hatchify-app 22 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/hatchify.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "streetsidesoftware.code-spell-checker", 6 | "davidanson.vscode-markdownlint", 7 | "firsttris.vscode-jest-runner", 8 | "ms-playwright.playwright" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "ts-node", 9 | "type": "node", 10 | "request": "launch", 11 | "args": ["${relativeFile}"], 12 | "runtimeArgs": ["-r", "ts-node/register"], 13 | "cwd": "${workspaceRoot}", 14 | "protocol": "inspector", 15 | "internalConsoleOptions": "openOnSessionStart" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Hatchify", "hatchifyjs"], 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "editor.formatOnSave": true, 8 | "jestrunner.jestCommand": "node --experimental-vm-modules ../../node_modules/.bin/jest", 9 | "search.exclude": { 10 | "**/coverage": true, 11 | "**/dist": true, 12 | "**/node_modules": true 13 | }, 14 | "typescript.tsdk": "node_modules/typescript/lib" 15 | } 16 | -------------------------------------------------------------------------------- /docs/attachments/FiltersClosed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/FiltersClosed.png -------------------------------------------------------------------------------- /docs/attachments/FiltersOpen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/FiltersOpen.png -------------------------------------------------------------------------------- /docs/attachments/List.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/List.png -------------------------------------------------------------------------------- /docs/attachments/ListWithPagination.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/ListWithPagination.png -------------------------------------------------------------------------------- /docs/attachments/add-postgres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/add-postgres.png -------------------------------------------------------------------------------- /docs/attachments/create-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/create-database.png -------------------------------------------------------------------------------- /docs/attachments/customNoRecords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/customNoRecords.png -------------------------------------------------------------------------------- /docs/attachments/data-grid-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/data-grid-example.png -------------------------------------------------------------------------------- /docs/attachments/dateonly-column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/dateonly-column.png -------------------------------------------------------------------------------- /docs/attachments/datetime-column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/datetime-column.png -------------------------------------------------------------------------------- /docs/attachments/defaultNoRecords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/defaultNoRecords.png -------------------------------------------------------------------------------- /docs/attachments/enum-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/enum-grid.png -------------------------------------------------------------------------------- /docs/attachments/frontend-todo-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/frontend-todo-list.png -------------------------------------------------------------------------------- /docs/attachments/integer-column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/integer-column.png -------------------------------------------------------------------------------- /docs/attachments/number-column.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/number-column.png -------------------------------------------------------------------------------- /docs/attachments/reactTs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/reactTs.gif -------------------------------------------------------------------------------- /docs/attachments/stringNullFilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/stringNullFilter.png -------------------------------------------------------------------------------- /docs/attachments/text-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/attachments/text-grid.png -------------------------------------------------------------------------------- /docs/jsonapi/deleting.md: -------------------------------------------------------------------------------- 1 | # Deleting Resources 2 | 3 | A resource can be deleted by sending a DELETE request to the URL that represents the resource: 4 | 5 | ```bash 6 | DELETE /api/photos/8ff0beed-2585-4391-8735-cc560eaf287e 7 | Accept: application/vnd.api+json 8 | ``` 9 | 10 | ## Responses 11 | 12 | `200 OK`: A server MAY return a 200 OK response with a document that contains no primary data if a deletion request is successful. Other top-level members, such as meta, could be included in the response document. 13 | -------------------------------------------------------------------------------- /docs/react/hatchedReact.Navigation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/docs/react/hatchedReact.Navigation.md -------------------------------------------------------------------------------- /e2e/main.txt: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.js"; 4 | import "./index.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /e2e/schemas.txt: -------------------------------------------------------------------------------- 1 | import { 2 | PartialSchema, 3 | belongsTo, 4 | dateonly, 5 | datetime, 6 | enumerate, 7 | hasMany, 8 | integer, 9 | string, 10 | text, 11 | } from "@hatchifyjs/core" 12 | 13 | export const Todo = { 14 | name: "Todo", 15 | attributes: { 16 | name: string(), 17 | dueDate: datetime(), 18 | importance: integer(), 19 | status: enumerate({ values: ["Pending", "Failed", "Completed"] }), 20 | completionDate: dateonly(), 21 | notes: text(), 22 | }, 23 | relationships: { 24 | user: belongsTo("User"), 25 | }, 26 | } satisfies PartialSchema 27 | 28 | export const User = { 29 | name: "User", 30 | attributes: { 31 | name: string(), 32 | }, 33 | relationships: { 34 | todos: hasMany(), 35 | }, 36 | } satisfies PartialSchema 37 | -------------------------------------------------------------------------------- /e2e/tests/react-rest-jsonapi.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test" 2 | 3 | test("works", async ({ page }) => { 4 | const messages: string[] = [] 5 | 6 | page.on( 7 | "console", 8 | (message) => message.type() === "assert" && messages.push(message.text()), 9 | ) 10 | 11 | await page.goto("http://localhost:5175", { waitUntil: "networkidle" }) 12 | 13 | await expect( 14 | page.getByRole("tab", { name: "Article", exact: true }), 15 | ).toBeVisible() 16 | await expect( 17 | page.getByRole("tab", { name: "Feature Article", exact: true }), 18 | ).toBeVisible() 19 | await expect( 20 | page.getByRole("tab", { name: "Admin User", exact: true }), 21 | ).toBeVisible() 22 | 23 | expect(messages).toEqual([]) 24 | }) 25 | -------------------------------------------------------------------------------- /e2e/tests/react-rest.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test" 2 | 3 | test("works", async ({ page }) => { 4 | await page.goto("http://localhost:5174") 5 | 6 | // * rows are visible 7 | await expect(page.getByText("Workout")).toBeVisible() 8 | await expect(page.getByText("Shopping")).toBeVisible() 9 | await expect(page.getByText("Cooking")).toBeVisible() 10 | 11 | // * submit a new todo 12 | await page.locator('input[type="text"]').fill("Homework") 13 | await page.locator('button[type="button"]').first().click() 14 | 15 | // * new todo is visible 16 | await expect(page.getByText("Homework")).toBeVisible() 17 | 18 | // * delete a todo 19 | await page.locator('button[type="button"]').nth(1).click() 20 | await expect(page.getByText("Workout")).not.toBeVisible() 21 | }) 22 | -------------------------------------------------------------------------------- /example/getting-started/.env: -------------------------------------------------------------------------------- 1 | DB_URI=sqlite://localhost/:memory -------------------------------------------------------------------------------- /example/getting-started/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /example/getting-started/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/getting-started/frontend/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /example/getting-started/frontend/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App.js" 4 | import "./index.css" 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /example/getting-started/frontend/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/getting-started/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/getting-started/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "execMap": { 4 | "ts": "node --loader ts-node/esm" 5 | } 6 | } -------------------------------------------------------------------------------- /example/getting-started/schemas.ts: -------------------------------------------------------------------------------- 1 | import { 2 | belongsTo, 3 | boolean, 4 | dateonly, 5 | integer, 6 | hasMany, 7 | string, 8 | } from "@hatchifyjs/core" 9 | import type { PartialSchema } from "@hatchifyjs/core" 10 | 11 | export const Todo = { 12 | name: "Todo", 13 | attributes: { 14 | name: string({ required: true }), 15 | dueDate: dateonly(), 16 | importance: integer(), 17 | complete: boolean({ default: false }), 18 | }, 19 | relationships: { 20 | user: belongsTo("User"), 21 | }, 22 | } satisfies PartialSchema 23 | 24 | export const User = { 25 | name: "User", 26 | attributes: { 27 | name: string({ required: true }), 28 | }, 29 | relationships: { 30 | todos: hasMany("Todo"), 31 | }, 32 | } satisfies PartialSchema 33 | -------------------------------------------------------------------------------- /example/getting-started/tsconfig.backend.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": [ 5 | "backend", 6 | "schemas.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/getting-started/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | optimizeDeps: { 7 | esbuildOptions: { 8 | target: "esnext", 9 | }, 10 | }, 11 | plugins: [react()], 12 | }) 13 | -------------------------------------------------------------------------------- /example/grid-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/grid-demo/Dockerfile: -------------------------------------------------------------------------------- 1 | ## this is a multi-stage build 2 | ## use --target backend/frontend to specify which stage to build 3 | ## otherwise only frontend builds 4 | ## using alpine and then installing node reduces size from 375MB to 269MB 5 | FROM alpine AS base 6 | WORKDIR /app 7 | RUN apk update && apk add npm nodejs~=20 && apk cache clean 8 | COPY package*.json ./ 9 | RUN npm ci 10 | COPY schemas.ts schemas.ts 11 | 12 | FROM base AS backend 13 | COPY backend/ backend/ 14 | COPY tsconfig*.json ./ 15 | COPY frontend/ frontend/ 16 | COPY public/ public/ 17 | COPY index.html tsconfig.json vite.config.ts ./ 18 | EXPOSE $APP_PORT 19 | CMD npm run "dev:$BACKEND:$DATABASE" 20 | -------------------------------------------------------------------------------- /example/grid-demo/README.md: -------------------------------------------------------------------------------- 1 | # Hatchify Grid Demo 2 | 3 | - - React Koa SQLite 4 | - - React Koa Postgres 5 | - - React Express SQLite 6 | - - React Express Postgres 7 | 8 | The demo is redeployed on every push to `main`. To deploy a version with the latest changes: 9 | 10 | 1. Create a new branch 11 | 2. Update the dependencies in `package.json` 12 | 13 | ```bash 14 | npm install @hatchifyjs/core@latest @hatchifyjs/express@latest @hatchifyjs/koa@latest @hatchifyjs/react@latest 15 | ``` 16 | 17 | 3. The demo will be deployed once the branch is merged into `main` 18 | -------------------------------------------------------------------------------- /example/grid-demo/frontend/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /example/grid-demo/frontend/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App.js" 4 | 5 | ReactDOM.createRoot(document.getElementById("root")!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /example/grid-demo/frontend/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/grid-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/grid-demo/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "execMap": { 4 | "ts": "node --loader ts-node/esm" 5 | } 6 | } -------------------------------------------------------------------------------- /example/grid-demo/tsconfig.backend.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": [ 5 | "backend", 6 | "schemas.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/grid-demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react" 2 | import { defineConfig } from "vite" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | optimizeDeps: { 7 | esbuildOptions: { 8 | target: "esnext", 9 | }, 10 | }, 11 | plugins: [react()], 12 | }) 13 | -------------------------------------------------------------------------------- /example/namespaces/.env: -------------------------------------------------------------------------------- 1 | DB_URI=sqlite://localhost/:memory -------------------------------------------------------------------------------- /example/namespaces/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /example/namespaces/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/namespaces/frontend/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /example/namespaces/frontend/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App.js" 4 | import "./index.css" 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /example/namespaces/frontend/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/namespaces/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/namespaces/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "execMap": { 4 | "ts": "node --loader ts-node/esm" 5 | } 6 | } -------------------------------------------------------------------------------- /example/namespaces/schemas.ts: -------------------------------------------------------------------------------- 1 | import { belongsTo, hasMany, string } from "@hatchifyjs/core" 2 | import type { PartialSchema } from "@hatchifyjs/core" 3 | 4 | export const Article = { 5 | name: "Article", 6 | ui: { displayAttribute: "author" }, 7 | attributes: { 8 | author: string({ required: true }), 9 | tag: string({ required: true }), 10 | }, 11 | } satisfies PartialSchema 12 | 13 | export const Feature_Article = { 14 | name: "Article", 15 | namespace: "Feature", 16 | ui: { displayAttribute: "author" }, 17 | attributes: { 18 | author: string({ required: true }), 19 | tag: string({ required: true }), 20 | }, 21 | relationships: { 22 | adminUser: belongsTo("Admin_User"), 23 | }, 24 | } satisfies PartialSchema 25 | 26 | export const Admin_User = { 27 | name: "User", 28 | namespace: "Admin", 29 | attributes: { 30 | name: string(), 31 | }, 32 | relationships: { 33 | articles: hasMany("Feature_Article"), 34 | }, 35 | } satisfies PartialSchema 36 | -------------------------------------------------------------------------------- /example/namespaces/tsconfig.backend.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": [ 5 | "backend", 6 | "schemas.ts" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/namespaces/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | optimizeDeps: { 7 | esbuildOptions: { 8 | target: "esnext", 9 | }, 10 | }, 11 | plugins: [react()], 12 | }) 13 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2020": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react-hooks/recommended" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["react-refresh"], 18 | "rules": { 19 | "react-refresh/only-export-components": "warn" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/backend/index.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from "path" 2 | import { fileURLToPath } from "url" 3 | import Koa from "koa" 4 | import c2k from "koa-connect" 5 | import { createServer as createViteServer } from "vite" 6 | import { hatchifyKoa } from "@hatchifyjs/koa" 7 | import * as schemas from "../schemas.js" 8 | 9 | const currentDir = dirname(fileURLToPath(import.meta.url)) 10 | 11 | const app = new Koa() 12 | const hatchedKoa = hatchifyKoa(schemas, { prefix: "/api" }) 13 | 14 | ;(async () => { 15 | await hatchedKoa.modelSync({ alter: true }) 16 | 17 | const vite = await createViteServer({ 18 | root: `${currentDir}/../`, 19 | server: { middlewareMode: true }, 20 | }) 21 | 22 | app.use(hatchedKoa.middleware.allModels.all) 23 | app.use(c2k(vite.middlewares)) 24 | 25 | app.listen(3000, () => { 26 | console.log("Started on http://localhost:3000") 27 | }) 28 | })() 29 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/frontend/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/frontend/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App.js" 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/frontend/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "ts", 3 | "execMap": { 4 | "ts": "node --loader ts-node/esm" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/schemas.ts: -------------------------------------------------------------------------------- 1 | import { belongsTo, hasMany, string } from "@hatchifyjs/core" 2 | import type { PartialSchema } from "@hatchifyjs/core" 3 | 4 | export const Todo = { 5 | name: "Todo", 6 | attributes: { 7 | name: string({ required: false }), 8 | }, 9 | relationships: { 10 | user: belongsTo("User"), 11 | }, 12 | } satisfies PartialSchema 13 | 14 | export const User = { 15 | name: "User", 16 | attributes: { 17 | name: string(), 18 | }, 19 | relationships: { 20 | todos: hasMany("Todo"), 21 | }, 22 | } satisfies PartialSchema 23 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/tsconfig.backend.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": {}, 4 | "include": ["backend", "schemas.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "experimentalResolver": true, 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "target": "ESNext", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "jsx": "react-jsx", 9 | 10 | // "composite": true, 11 | // "declaration": true, 12 | "incremental": true, 13 | "strict": true, 14 | 15 | "allowJs": false, 16 | "allowSyntheticDefaultImports": true, 17 | "downlevelIteration": true, 18 | "esModuleInterop": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "isolatedModules": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitReturns": true, 23 | "noUnusedLocals": true, 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "useDefineForClassFields": true 27 | }, 28 | "include": ["frontend", "backend", "schemas.ts", "vite.config.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /example/react-rest-hatchify-backend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import react from "@vitejs/plugin-react" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /example/react-rest/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /example/react-rest/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/react-rest/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/react-rest/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /example/react-rest/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App.js" 4 | 5 | if (process.env.NODE_ENV === "development") { 6 | await import("./mocks/browser").then(({ worker }) => { 7 | worker.start() 8 | }) 9 | } 10 | 11 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 12 | 13 | 14 | , 15 | ) 16 | -------------------------------------------------------------------------------- /example/react-rest/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/react-rest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /example/react-rest/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /example/react-rest/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import react from "@vitejs/plugin-react" 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 5174, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "nx/presets/npm.json", 3 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 4 | "tasksRunnerOptions": { 5 | "default": { 6 | "runner": "nx/tasks-runners/default", 7 | "options": { 8 | "cacheableOperations": [ 9 | "typecheck", 10 | "lint", 11 | "depcheck", 12 | "test", 13 | "clean", 14 | "build" 15 | ] 16 | } 17 | } 18 | }, 19 | "targetDefaults": { 20 | "typecheck": { 21 | "dependsOn": ["^build"] 22 | }, 23 | "test": { 24 | "dependsOn": ["^build"] 25 | }, 26 | "build": { 27 | "dependsOn": ["^build", "clean"] 28 | }, 29 | "dev": { 30 | "dependsOn": ["^build"] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignore-dirs: 2 | - /dist 3 | ignores: 4 | - "@types/*" 5 | - ts-jest 6 | -------------------------------------------------------------------------------- /packages/core/.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | `@hatchifyjs/core` provides core libraries and types for defining Schemas in Hatchify. 2 | 3 | For more information, see [`@hatchifyjs/core`'s documentation page](https://github.com/bitovi/hatchify/blob/main/docs/core/README.md) -------------------------------------------------------------------------------- /packages/core/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverageFrom": [ 3 | "/src/**/*.ts", 4 | "!/src/**/types.ts", 5 | "!/src/dataTypes/index.ts", 6 | "!/src/core.ts", 7 | "!/src/relationships/index.ts", 8 | "!/src/types/**/*.ts" 9 | ], 10 | "coverageDirectory": "../../coverage/core", 11 | "coverageReporters": ["html", "text", "text-summary"], 12 | "coverageThreshold": { 13 | "global": { 14 | "branches": 100, 15 | "functions": 100, 16 | "lines": 100, 17 | "statements": 100 18 | } 19 | }, 20 | "extensionsToTreatAsEsm": [".ts"], 21 | "moduleNameMapper": { 22 | "^(\\.{1,2}/.*)\\.js$": "$1" 23 | }, 24 | "roots": ["/src"], 25 | "testEnvironment": "node", 26 | "transform": { 27 | ".+\\.ts$": [ 28 | "ts-jest", 29 | { 30 | "useESM": true 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/assembler/ensureDefaultPrimaryAttribute.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultPrimaryAttribute } from "./getDefaultPrimaryAttribute.js" 2 | import type { 3 | PartialAttributeOptions, 4 | PartialSchema, 5 | PartialSchemaWithPrimaryAttribute, 6 | } from "./types.js" 7 | 8 | export function ensureDefaultPrimaryAttribute( 9 | schema: PartialSchema, 10 | ): PartialSchemaWithPrimaryAttribute { 11 | return { 12 | ...schema, 13 | id: schema.id 14 | ? ({ 15 | ...schema.id, 16 | control: { 17 | ...schema.id.control, 18 | primary: true, 19 | allowNull: false, 20 | }, 21 | orm: { 22 | ...schema.id.orm, 23 | sequelize: { 24 | ...schema.id.orm.sequelize, 25 | primaryKey: true, 26 | allowNull: false, 27 | }, 28 | }, 29 | } as PartialAttributeOptions) 30 | : getDefaultPrimaryAttribute(), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/assembler/ensureDefaultReadOnlyAttribute.spec.ts: -------------------------------------------------------------------------------- 1 | import { ensureDefaultReadOnlyAttribute } from "./ensureDefaultReadOnlyAttribute.js" 2 | import { string } from "../dataTypes/index.js" 3 | 4 | describe("ensureDefaultReadOnlyAttribute", () => { 5 | it("handles existing read-only schema", () => { 6 | expect( 7 | JSON.stringify( 8 | ensureDefaultReadOnlyAttribute({ 9 | name: "Todo", 10 | attributes: { 11 | name: string({ readOnly: true }), 12 | description: string(), 13 | }, 14 | readOnly: true, 15 | }), 16 | ), 17 | ).toEqual( 18 | JSON.stringify({ 19 | name: "Todo", 20 | attributes: { 21 | name: string({ readOnly: true }), 22 | description: { 23 | ...string({ readOnly: true }), 24 | name: "string()", 25 | }, 26 | }, 27 | readOnly: true, 28 | }), 29 | ) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/core/src/assembler/ensureDefaultReadOnlyAttribute.ts: -------------------------------------------------------------------------------- 1 | import type { PartialAttributeRecord, PartialSchema } from "./types.js" 2 | 3 | export function ensureDefaultReadOnlyAttribute( 4 | schema: PartialSchema, 5 | ): PartialSchema { 6 | return { 7 | ...schema, 8 | attributes: Object.entries(schema.attributes).reduce( 9 | (acc, [key, value]) => ({ 10 | ...acc, 11 | [key]: { 12 | ...value, 13 | control: { 14 | ...value.control, 15 | readOnly: value.control.readOnly || schema.readOnly, 16 | }, 17 | }, 18 | }), 19 | {} as PartialAttributeRecord, 20 | ), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/assembler/finalizeSchema.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialSchemaWithPrimaryAttribute, 3 | SemiFinalSchema, 4 | } from "./types.js" 5 | 6 | export function finalizeSchema( 7 | schema: PartialSchemaWithPrimaryAttribute, 8 | ): SemiFinalSchema { 9 | return { 10 | ...schema, 11 | id: schema.id.finalize(), 12 | ui: schema.ui ?? {}, 13 | attributes: Object.entries(schema.attributes).reduce( 14 | (acc, [attributeName, attribute]) => ({ 15 | ...acc, 16 | [attributeName]: attribute.finalize(), 17 | }), 18 | {}, 19 | ), 20 | readOnly: !!schema.readOnly, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/assembler/getDefaultPrimaryAttribute.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from "../dataTypes/index.js" 2 | import type { PartialAttributeOptions } from "../types/index.js" 3 | import { uuidv4 } from "../util/uuidv4.js" 4 | 5 | export function getDefaultPrimaryAttribute(): PartialAttributeOptions { 6 | return uuid({ 7 | primary: true, 8 | required: true, 9 | default: uuidv4, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/core.ts: -------------------------------------------------------------------------------- 1 | export * from "./types/index.js" 2 | export { assembler } from "./assembler/index.js" 3 | export * from "./dataTypes/index.js" 4 | export * from "./relationships/index.js" 5 | export * from "./util/camelCaseToPascalCase.js" 6 | export * from "./util/camelCaseToTitleCase.js" 7 | export * from "./util/getEndpoint.js" 8 | export * from "./util/getSchemaKey.js" 9 | export * from "./util/mergeSchemaUI.js" 10 | export * from "./util/pascalCaseToCamelCase.js" 11 | export * from "./util/pascalCaseToKebabCase.js" 12 | export * from "./util/pluralize.js" 13 | export * from "./util/singularize.js" 14 | export * from "./util/uuidv4.js" 15 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/boolean/coerce.ts: -------------------------------------------------------------------------------- 1 | import type { PartialBooleanControlType } from "./types.js" 2 | import type { ValueInRequest } from "../../types/index.js" 3 | import { HatchifyCoerceError } from "../../types/index.js" 4 | 5 | export function coerce( 6 | value: ValueInRequest, 7 | control: Omit, "allowNullInfer">, 8 | ): boolean | null { 9 | if (value === undefined) { 10 | throw new HatchifyCoerceError("as a non-undefined value") 11 | } 12 | 13 | if (value === null) { 14 | if (control.allowNull) { 15 | return value 16 | } 17 | throw new HatchifyCoerceError("as a non-null value") 18 | } 19 | 20 | if (typeof value !== "boolean") { 21 | throw new HatchifyCoerceError("as a boolean") 22 | } 23 | 24 | return value 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/boolean/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialBooleanControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false, 11 | primary: !!props.primary, 12 | default: props.default ?? null, 13 | readOnly: props.readOnly ?? false, 14 | ui: { 15 | displayName: props?.ui?.displayName ?? null, 16 | hidden: props?.ui?.hidden ?? false, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/boolean/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalBooleanORM, PartialBooleanORM } from "./types.js" 2 | 3 | export function finalizeOrm({ sequelize }: PartialBooleanORM): FinalBooleanORM { 4 | return { 5 | sequelize: { 6 | ...sequelize, 7 | allowNull: sequelize.allowNull !== false, 8 | primaryKey: !!sequelize.primaryKey, 9 | defaultValue: sequelize.defaultValue ?? null, 10 | unique: !!sequelize.unique, 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/boolean/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { boolean } from "./index.js" 2 | 3 | describe("getFinalize", () => { 4 | it("finalizes a partial attribute", () => { 5 | expect(boolean()).toEqual({ 6 | name: "boolean()", 7 | control: { 8 | allowNull: undefined, 9 | type: "Boolean", 10 | ui: {}, 11 | }, 12 | orm: { 13 | sequelize: { 14 | allowNull: undefined, 15 | type: "BOOLEAN", 16 | }, 17 | }, 18 | finalize: expect.any(Function), 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/boolean/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialBooleanControlType, PartialBooleanProps } from "./types.js" 2 | 3 | export function getPartialControl( 4 | props?: PartialBooleanProps, 5 | ): PartialBooleanControlType { 6 | return { 7 | type: "Boolean", 8 | allowNull: props?.required == null ? props?.required : !props.required, 9 | allowNullInfer: (props?.required == null 10 | ? props?.required 11 | : !props.required) as TRequired extends true ? false : true, 12 | primary: props?.primary, 13 | default: props?.default, 14 | readOnly: props?.readOnly, 15 | ui: { 16 | displayName: props?.ui?.displayName, 17 | hidden: props?.ui?.hidden, 18 | }, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/boolean/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialBooleanORM, PartialBooleanProps } from "./types.js" 2 | 3 | export function getPartialOrm( 4 | props?: PartialBooleanProps, 5 | ): PartialBooleanORM { 6 | return { 7 | sequelize: { 8 | type: "BOOLEAN", 9 | allowNull: props?.required == null ? props?.required : !props.required, 10 | primaryKey: props?.primary, 11 | defaultValue: props?.default, 12 | unique: props?.unique, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/boolean/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialControlType, 3 | PartialDataTypeProps, 4 | PartialSequelizeDataType, 5 | } from "../../types/index.js" 6 | 7 | export type PartialBooleanProps = 8 | PartialDataTypeProps 9 | 10 | export interface PartialBooleanControlType 11 | extends PartialControlType { 12 | type: "Boolean" 13 | } 14 | 15 | export interface PartialBooleanORM { 16 | sequelize: Omit, "typeArgs"> 17 | } 18 | 19 | export interface FinalBooleanORM { 20 | sequelize: Required< 21 | Omit, "typeArgs"> 22 | > 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialDateonlyControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false && !props.primary, 11 | min: props.min ?? -Infinity, 12 | max: props.max ?? Infinity, 13 | primary: !!props.primary, 14 | default: props.default ?? null, 15 | step: props.step || 0, 16 | readOnly: props.readOnly ?? false, 17 | ui: { 18 | displayName: props?.ui?.displayName ?? null, 19 | hidden: props?.ui?.hidden ?? false, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalDateonlyORM, PartialDateonlyORM } from "./types.js" 2 | 3 | export function finalizeOrm({ 4 | sequelize, 5 | }: PartialDateonlyORM): FinalDateonlyORM { 6 | return { 7 | sequelize: { 8 | ...sequelize, 9 | allowNull: sequelize.allowNull !== false && !sequelize.primaryKey, 10 | primaryKey: !!sequelize.primaryKey, 11 | defaultValue: sequelize.defaultValue ?? null, 12 | unique: !!sequelize.unique || !!sequelize.primaryKey, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { dateonly } from "./index.js" 2 | 3 | describe("getFinalize", () => { 4 | it("finalizes a partial attribute", () => { 5 | expect(dateonly()).toEqual({ 6 | name: "dateonly()", 7 | control: { 8 | allowNull: undefined, 9 | max: undefined, 10 | min: undefined, 11 | type: "Dateonly", 12 | primary: undefined, 13 | ui: {}, 14 | }, 15 | orm: { 16 | sequelize: { 17 | allowNull: undefined, 18 | type: "DATEONLY", 19 | typeArgs: [], 20 | primaryKey: undefined, 21 | }, 22 | }, 23 | finalize: expect.any(Function), 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialDateonlyControlType, 3 | PartialDateonlyProps, 4 | } from "./types.js" 5 | 6 | export function getPartialControl( 7 | props?: PartialDateonlyProps, 8 | ): PartialDateonlyControlType { 9 | return { 10 | type: "Dateonly", 11 | allowNull: props?.required == null ? props?.required : !props.required, 12 | allowNullInfer: (props?.required == null 13 | ? props?.required 14 | : !props.required) as TRequired extends true ? false : true, 15 | min: props?.min, 16 | max: props?.max, 17 | primary: props?.primary, 18 | step: props?.step, 19 | default: props?.default, 20 | readOnly: props?.readOnly, 21 | ui: { 22 | displayName: props?.ui?.displayName, 23 | hidden: props?.ui?.hidden, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialDateonlyORM, PartialDateonlyProps } from "./types.js" 2 | 3 | export function getPartialOrm( 4 | props?: PartialDateonlyProps, 5 | ): PartialDateonlyORM { 6 | return { 7 | sequelize: { 8 | type: "DATEONLY", 9 | typeArgs: [], 10 | allowNull: props?.required == null ? props?.required : !props.required, 11 | primaryKey: props?.primary, 12 | defaultValue: props?.default, 13 | unique: props?.unique, 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/isISO8601DateString.spec.ts: -------------------------------------------------------------------------------- 1 | import { isISO8601DateString } from "./isISO8601DateString.js" 2 | 3 | describe("isISO8601DateString", () => { 4 | it("validates date strings correctly", () => { 5 | expect(isISO8601DateString("2011-10-05")).toBe(true) 6 | expect(isISO8601DateString("")).toBe(false) 7 | expect(isISO8601DateString("2011-10-05T14:48:00.000Z")).toBe(false) 8 | expect(isISO8601DateString("2018-11-10T11:22:33+00:00")).toBe(false) 9 | expect(isISO8601DateString("2011-10-05T14:99:00.000Z")).toBe(false) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/isISO8601DateString.ts: -------------------------------------------------------------------------------- 1 | export function isISO8601DateString(string: string): boolean { 2 | if (!/\d{4}-\d{2}-\d{2}/.test(string)) { 3 | return false 4 | } 5 | 6 | const date = new Date(string) 7 | 8 | return ( 9 | date instanceof Date && 10 | !isNaN(date.getTime()) && 11 | date.toISOString() === `${string}T00:00:00.000Z` 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/dateonly/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialControlType, 3 | PartialDataTypeProps, 4 | PartialSequelizeDataType, 5 | } from "../../types/index.js" 6 | 7 | export type StringStep = "day" | "week" | "year" | "decade" 8 | 9 | export interface PartialDateonlyProps 10 | extends PartialDataTypeProps { 11 | min?: string | typeof Infinity 12 | max?: string | typeof Infinity 13 | step?: StringStep | number 14 | } 15 | 16 | export interface PartialDateonlyControlType 17 | extends PartialControlType { 18 | type: "Dateonly" 19 | min?: string | typeof Infinity 20 | max?: string | typeof Infinity 21 | step?: StringStep | number 22 | } 23 | 24 | export interface PartialDateonlyORM { 25 | sequelize: PartialSequelizeDataType 26 | } 27 | 28 | export interface FinalDateonlyORM { 29 | sequelize: Required> 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/datetime/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialDatetimeControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false && !props.primary, 11 | min: props.min ?? -Infinity, 12 | max: props.max ?? Infinity, 13 | primary: !!props.primary, 14 | default: props.default ?? null, 15 | step: props.step || 0, 16 | readOnly: props.readOnly ?? false, 17 | ui: { 18 | displayName: props?.ui?.displayName ?? null, 19 | hidden: props?.ui?.hidden ?? false, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/datetime/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalDatetimeORM, PartialDatetimeORM } from "./types.js" 2 | 3 | export function finalizeOrm({ 4 | sequelize, 5 | }: PartialDatetimeORM): FinalDatetimeORM { 6 | return { 7 | sequelize: { 8 | ...sequelize, 9 | allowNull: sequelize.allowNull !== false && !sequelize.primaryKey, 10 | primaryKey: !!sequelize.primaryKey, 11 | defaultValue: sequelize.defaultValue ?? null, 12 | unique: !!sequelize.unique || !!sequelize.primaryKey, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/datetime/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { datetime } from "./index.js" 2 | 3 | describe("getFinalize", () => { 4 | it("finalizes a partial attribute", () => { 5 | expect(datetime()).toEqual({ 6 | name: "datetime()", 7 | control: { 8 | allowNull: undefined, 9 | max: undefined, 10 | min: undefined, 11 | primary: undefined, 12 | type: "Date", 13 | ui: {}, 14 | }, 15 | orm: { 16 | sequelize: { 17 | allowNull: undefined, 18 | primaryKey: undefined, 19 | type: "DATE", 20 | typeArgs: [], 21 | }, 22 | }, 23 | finalize: expect.any(Function), 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/datetime/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialDatetimeControlType, 3 | PartialDatetimeProps, 4 | } from "./types.js" 5 | 6 | export function getPartialControl( 7 | props?: PartialDatetimeProps, 8 | ): PartialDatetimeControlType { 9 | return { 10 | type: "Date", 11 | allowNull: props?.required == null ? props?.required : !props.required, 12 | allowNullInfer: (props?.required == null 13 | ? props?.required 14 | : !props.required) as TRequired extends true ? false : true, 15 | min: props?.min, 16 | max: props?.max, 17 | primary: props?.primary, 18 | step: props?.step, 19 | default: props?.default, 20 | readOnly: props?.readOnly, 21 | ui: { 22 | displayName: props?.ui?.displayName, 23 | hidden: props?.ui?.hidden, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/datetime/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialDatetimeORM, PartialDatetimeProps } from "./types.js" 2 | 3 | export function getPartialOrm( 4 | props?: PartialDatetimeProps, 5 | ): PartialDatetimeORM { 6 | return { 7 | sequelize: { 8 | type: "DATE", 9 | typeArgs: [], 10 | allowNull: props?.required == null ? props?.required : !props.required, 11 | primaryKey: props?.primary, 12 | defaultValue: props?.default, 13 | unique: props?.unique, 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/datetime/isISO8601DatetimeString.spec.ts: -------------------------------------------------------------------------------- 1 | import { isISO8601DatetimeString } from "./isISO8601DatetimeString.js" 2 | 3 | describe("isISO8601DatetimeString", () => { 4 | it("validates date strings correctly", () => { 5 | expect(isISO8601DatetimeString("2011-10-05T14:48:00.000Z")).toBe(true) 6 | expect(isISO8601DatetimeString("2018-11-10T11:22:33+00:00")).toBe(false) 7 | expect(isISO8601DatetimeString("2011-10-05T14:99:00.000Z")).toBe(false) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/datetime/isISO8601DatetimeString.ts: -------------------------------------------------------------------------------- 1 | export function isISO8601DatetimeString(string: string): boolean { 2 | if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(string)) { 3 | return false 4 | } 5 | 6 | const date = new Date(string) 7 | 8 | return ( 9 | date instanceof Date && 10 | !isNaN(date.getTime()) && 11 | date.toISOString() === string 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/enumerate/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialEnumControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false && !props.primary, 11 | primary: !!props.primary, 12 | default: props.default ?? null, 13 | readOnly: props.readOnly ?? false, 14 | ui: { 15 | displayName: props?.ui?.displayName ?? null, 16 | hidden: props?.ui?.hidden ?? false, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/enumerate/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalEnumORM, PartialEnumORM } from "./types.js" 2 | 3 | export function finalizeOrm({ sequelize }: PartialEnumORM): FinalEnumORM { 4 | return { 5 | sequelize: { 6 | ...sequelize, 7 | allowNull: sequelize.allowNull !== false && !sequelize.primaryKey, 8 | primaryKey: !!sequelize.primaryKey, 9 | defaultValue: sequelize.defaultValue ?? null, 10 | unique: !!sequelize.unique || !!sequelize.primaryKey, 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/enumerate/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { enumerate } from "./index.js" 2 | 3 | describe("getFinalize", () => { 4 | const values = ["foo", "bar"] 5 | 6 | it("finalizes a partial attribute", () => { 7 | expect(enumerate({ values })).toEqual({ 8 | name: 'enumerate({"values":["foo","bar"]})', 9 | control: { 10 | allowNull: undefined, 11 | primary: undefined, 12 | type: "enum", 13 | values, 14 | ui: {}, 15 | }, 16 | orm: { 17 | sequelize: { 18 | allowNull: undefined, 19 | primaryKey: undefined, 20 | type: "ENUM", 21 | typeArgs: values, 22 | }, 23 | }, 24 | finalize: expect.any(Function), 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/enumerate/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialEnumORM, PartialEnumProps } from "./types.js" 2 | import { validateValues } from "./validateValues.js" 3 | import { HatchifyInvalidSchemaError } from "../../types/index.js" 4 | 5 | export function getPartialOrm( 6 | // @todo HATCH-417 7 | props: PartialEnumProps, 8 | ): PartialEnumORM { 9 | if (!validateValues(props.values)) { 10 | throw new HatchifyInvalidSchemaError( 11 | "enum must be called with values as a non-empty string array", 12 | ) 13 | } 14 | 15 | return { 16 | sequelize: { 17 | type: "ENUM", 18 | typeArgs: props.values, 19 | allowNull: props?.required == null ? props?.required : !props.required, 20 | primaryKey: props?.primary, 21 | defaultValue: props?.default, 22 | unique: props?.unique, 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/enumerate/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialControlType, 3 | PartialDataTypeProps, 4 | PartialSequelizeDataType, 5 | } from "../../types/index.js" 6 | 7 | export interface PartialEnumProps< 8 | TRequired extends boolean, 9 | TValues extends readonly string[], 10 | > extends PartialDataTypeProps { 11 | values: TValues 12 | } 13 | 14 | export interface PartialEnumControlType< 15 | TRequired extends boolean, 16 | TValues extends readonly string[], 17 | > extends PartialControlType { 18 | type: "enum" 19 | values: TValues 20 | } 21 | 22 | export interface PartialEnumORM { 23 | sequelize: PartialSequelizeDataType 24 | } 25 | 26 | export interface FinalEnumORM { 27 | sequelize: Required> 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/enumerate/validateValues.spec.ts: -------------------------------------------------------------------------------- 1 | import { validateValues } from "./validateValues.js" 2 | 3 | describe("validateValues", () => { 4 | it("validates an array of strings", () => { 5 | expect(validateValues(["foo"])).toBe(true) 6 | expect(validateValues(["foo", "bar"])).toBe(true) 7 | expect(validateValues(null as unknown as string[])).toBe(false) 8 | expect(validateValues(undefined as unknown as string[])).toBe(false) 9 | expect(validateValues(1 as unknown as string[])).toBe(false) 10 | expect(validateValues("string" as unknown as string[])).toBe(false) 11 | expect(validateValues({} as unknown as string[])).toBe(false) 12 | expect(validateValues({ foo: "bar" } as unknown as string[])).toBe(false) 13 | expect( 14 | validateValues((() => { 15 | /* noop */ 16 | }) as unknown as string[]), 17 | ).toBe(false) 18 | expect(validateValues([])).toBe(false) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/enumerate/validateValues.ts: -------------------------------------------------------------------------------- 1 | export function validateValues(values: readonly string[]): boolean { 2 | return ( 3 | Array.isArray(values) && 4 | values.length !== 0 && 5 | values.every((value) => typeof value === "string") 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/integer/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialIntegerProps } from "./types.js" 2 | import { getPartialControl as getNumberPartialControl } from "../number/index.js" 3 | import type { PartialNumberControlType } from "../number/types.js" 4 | 5 | export function getPartialControl( 6 | props?: PartialIntegerProps, 7 | ): PartialNumberControlType { 8 | return getNumberPartialControl({ ...props, step: 1 }) 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/integer/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialIntegerProps } from "./types.js" 2 | import { getPartialOrm as getNumberPartialOrm } from "../number/index.js" 3 | import type { PartialNumberORM } from "../number/index.js" 4 | 5 | export function getPartialOrm( 6 | props?: PartialIntegerProps, 7 | ): PartialNumberORM { 8 | const numberOrm = getNumberPartialOrm(props) 9 | 10 | return { 11 | ...numberOrm, 12 | sequelize: { 13 | ...numberOrm.sequelize, 14 | type: "INTEGER", 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/integer/types.ts: -------------------------------------------------------------------------------- 1 | import type { PartialNumberProps } from "../number/index.js" 2 | 3 | export type PartialIntegerProps = Omit< 4 | PartialNumberProps, 5 | "step" 6 | > 7 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/number/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialNumberControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false && !props.primary, 11 | min: props.min ?? -Infinity, 12 | max: props.max ?? Infinity, 13 | primary: !!props.primary, 14 | step: props.step || 0, 15 | default: props.default ?? null, 16 | readOnly: props.readOnly ?? false, 17 | ui: { 18 | displayName: props?.ui?.displayName ?? null, 19 | hidden: props?.ui?.hidden ?? false, 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/number/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalNumberORM, PartialNumberORM } from "./types.js" 2 | 3 | export function finalizeOrm({ sequelize }: PartialNumberORM): FinalNumberORM { 4 | return { 5 | sequelize: { 6 | ...sequelize, 7 | allowNull: sequelize.allowNull !== false && !sequelize.primaryKey, 8 | autoIncrement: !!sequelize.autoIncrement, 9 | primaryKey: !!sequelize.primaryKey, 10 | defaultValue: sequelize.defaultValue ?? null, 11 | unique: !!sequelize.unique || !!sequelize.primaryKey, 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/number/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { number } from "./index.js" 2 | 3 | describe("getFinalize", () => { 4 | it("finalizes a partial attribute", () => { 5 | expect(number()).toEqual({ 6 | name: "number()", 7 | control: { 8 | allowNull: undefined, 9 | max: undefined, 10 | min: undefined, 11 | primary: undefined, 12 | step: undefined, 13 | type: "Number", 14 | ui: {}, 15 | }, 16 | orm: { 17 | sequelize: { 18 | allowNull: undefined, 19 | autoIncrement: undefined, 20 | primaryKey: undefined, 21 | type: "DECIMAL", 22 | typeArgs: [], 23 | }, 24 | }, 25 | finalize: expect.any(Function), 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/number/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialNumberControlType, PartialNumberProps } from "./types.js" 2 | 3 | export function getPartialControl( 4 | props?: PartialNumberProps, 5 | ): PartialNumberControlType { 6 | return { 7 | type: "Number", 8 | allowNull: props?.required == null ? props?.required : !props.required, 9 | allowNullInfer: (props?.required == null 10 | ? props?.required 11 | : !props.required) as TRequired extends true ? false : true, 12 | min: props?.min, 13 | max: props?.max, 14 | primary: props?.primary, 15 | step: props?.step, 16 | default: props?.default, 17 | readOnly: props?.readOnly, 18 | ui: { 19 | displayName: props?.ui?.displayName, 20 | hidden: props?.ui?.hidden, 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/number/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialNumberORM, PartialNumberProps } from "./types.js" 2 | 3 | export function getPartialOrm( 4 | props?: PartialNumberProps, 5 | ): PartialNumberORM { 6 | return { 7 | sequelize: { 8 | type: "DECIMAL", 9 | typeArgs: [], 10 | allowNull: props?.required == null ? props?.required : !props.required, 11 | autoIncrement: props?.autoIncrement, 12 | primaryKey: props?.primary, 13 | defaultValue: props?.default, 14 | unique: props?.unique, 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/number/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialControlType, 3 | PartialDataTypeProps, 4 | PartialSequelizeDataType, 5 | } from "../../types/index.js" 6 | 7 | export interface PartialNumberProps 8 | extends PartialDataTypeProps { 9 | autoIncrement?: boolean 10 | min?: number 11 | max?: number 12 | step?: number 13 | } 14 | 15 | export interface PartialNumberControlType 16 | extends PartialControlType { 17 | type: "Number" 18 | min?: number 19 | max?: number 20 | step?: number 21 | } 22 | 23 | export interface PartialNumberORM { 24 | sequelize: PartialSequelizeDataType & { 25 | autoIncrement?: boolean 26 | } 27 | } 28 | 29 | export interface FinalNumberORM { 30 | sequelize: Required> & { 31 | autoIncrement: boolean 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/number/validateStep.ts: -------------------------------------------------------------------------------- 1 | export function validateStep( 2 | value: number, 3 | step?: number, 4 | min?: number, 5 | ): boolean { 6 | if (value == null) { 7 | return false 8 | } 9 | 10 | if (!step) { 11 | return true 12 | } 13 | 14 | const diff = (!min || min === -Infinity ? value : value - min) % step 15 | const epsilon = 0.00001 // a small tolerance 16 | const absDiff = diff < 0 ? -diff : diff 17 | return absDiff < epsilon || step - absDiff < epsilon 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/string/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialStringControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false && !props.primary, 11 | min: props.min ?? 0, 12 | max: props.max ?? 255, 13 | primary: !!props.primary, 14 | default: props.default ?? null, 15 | regex: props.regex ?? /(.*?)/, 16 | readOnly: props.readOnly ?? false, 17 | ui: { 18 | enableCaseSensitiveContains: 19 | props.ui?.enableCaseSensitiveContains ?? false, 20 | maxDisplayLength: props?.ui?.maxDisplayLength ?? null, 21 | displayName: props?.ui?.displayName ?? null, 22 | hidden: props?.ui?.hidden ?? false, 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/string/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalStringORM, PartialStringORM } from "./types.js" 2 | 3 | export function finalizeOrm({ sequelize }: PartialStringORM): FinalStringORM { 4 | return { 5 | sequelize: { 6 | ...sequelize, 7 | typeArgs: sequelize.typeArgs.length ? sequelize.typeArgs : [255], 8 | allowNull: sequelize.allowNull !== false && !sequelize.primaryKey, 9 | primaryKey: !!sequelize.primaryKey, 10 | defaultValue: sequelize.defaultValue ?? null, 11 | unique: !!sequelize.unique || !!sequelize.primaryKey, 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/string/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { string } from "./index.js" 2 | 3 | describe("getFinalize", () => { 4 | it("finalizes a partial attribute", () => { 5 | expect(string()).toEqual({ 6 | name: "string()", 7 | control: { 8 | allowNull: undefined, 9 | max: undefined, 10 | min: undefined, 11 | primary: undefined, 12 | regex: undefined, 13 | type: "String", 14 | ui: {}, 15 | }, 16 | orm: { 17 | sequelize: { 18 | allowNull: undefined, 19 | primaryKey: undefined, 20 | type: "STRING", 21 | typeArgs: [], 22 | }, 23 | }, 24 | finalize: expect.any(Function), 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/string/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialStringControlType, PartialStringProps } from "./types.js" 2 | 3 | export function getPartialControl( 4 | props?: PartialStringProps, 5 | ): PartialStringControlType { 6 | return { 7 | type: "String", 8 | allowNull: props?.required == null ? props?.required : !props.required, 9 | allowNullInfer: (props?.required == null 10 | ? props?.required 11 | : !props.required) as TRequired extends true ? false : true, 12 | min: props?.min, 13 | max: props?.max, 14 | primary: props?.primary, 15 | default: props?.default, 16 | regex: props?.regex, 17 | readOnly: props?.readOnly, 18 | ui: { 19 | enableCaseSensitiveContains: props?.ui?.enableCaseSensitiveContains, 20 | maxDisplayLength: props?.ui?.maxDisplayLength, 21 | displayName: props?.ui?.displayName, 22 | hidden: props?.ui?.hidden, 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/string/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialStringORM, PartialStringProps } from "./types.js" 2 | 3 | export function getPartialOrm( 4 | props?: PartialStringProps, 5 | ): PartialStringORM { 6 | return { 7 | sequelize: { 8 | type: "STRING", 9 | typeArgs: props?.max == null || props.max === Infinity ? [] : [props.max], 10 | allowNull: props?.required == null ? props?.required : !props.required, 11 | primaryKey: props?.primary, 12 | defaultValue: props?.default, 13 | unique: props?.unique, 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/text/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialTextControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false && !props.primary, 11 | min: props.min ?? 0, 12 | max: props.max ?? Infinity, 13 | primary: !!props.primary, 14 | default: props.default ?? null, 15 | regex: props.regex ?? /(.*?)/, 16 | readOnly: props.readOnly ?? false, 17 | ui: { 18 | enableCaseSensitiveContains: 19 | props.ui?.enableCaseSensitiveContains ?? false, 20 | displayName: props?.ui?.displayName ?? null, 21 | maxDisplayLength: props?.ui?.maxDisplayLength ?? null, 22 | hidden: props?.ui?.hidden ?? false, 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/text/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalTextORM, PartialTextORM } from "./types.js" 2 | 3 | export function finalizeOrm({ sequelize }: PartialTextORM): FinalTextORM { 4 | return { 5 | sequelize: { 6 | ...sequelize, 7 | allowNull: sequelize.allowNull !== false && !sequelize.primaryKey, 8 | primaryKey: !!sequelize.primaryKey, 9 | defaultValue: sequelize.defaultValue ?? null, 10 | unique: !!sequelize.unique || !!sequelize.primaryKey, 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/text/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { text } from "./index.js" 2 | 3 | describe("getFinalize", () => { 4 | it("finalizes a partial attribute", () => { 5 | expect(text()).toEqual({ 6 | name: "text()", 7 | control: { 8 | allowNull: undefined, 9 | max: Infinity, 10 | min: 0, 11 | primary: undefined, 12 | regex: undefined, 13 | type: "String", 14 | ui: {}, 15 | }, 16 | orm: { 17 | sequelize: { 18 | allowNull: undefined, 19 | primaryKey: undefined, 20 | type: "TEXT", 21 | }, 22 | }, 23 | finalize: expect.any(Function), 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/text/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialTextProps } from "./types.js" 2 | import { getPartialControl as getStringPartialControl } from "../string/index.js" 3 | import type { PartialStringControlType } from "../string/types.js" 4 | 5 | export function getPartialControl( 6 | props?: PartialTextProps, 7 | ): PartialStringControlType { 8 | return getStringPartialControl({ 9 | ...props, 10 | min: 0, 11 | max: Infinity, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/text/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialTextORM, PartialTextProps } from "./types.js" 2 | 3 | export function getPartialOrm( 4 | props?: PartialTextProps, 5 | ): PartialTextORM { 6 | return { 7 | sequelize: { 8 | type: "TEXT", 9 | allowNull: props?.required == null ? props?.required : !props.required, 10 | primaryKey: props?.primary, 11 | defaultValue: props?.default, 12 | unique: props?.unique, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/text/index.ts: -------------------------------------------------------------------------------- 1 | import { getFinalize } from "./getFinalize.js" 2 | import { getPartialControl } from "./getPartialControl.js" 3 | import { getPartialOrm } from "./getPartialOrm.js" 4 | import type { 5 | FinalTextORM, 6 | PartialTextControlType, 7 | PartialTextORM, 8 | PartialTextProps, 9 | } from "./types.js" 10 | import type { PartialAttribute } from "../../types/index.js" 11 | 12 | export function text( 13 | props?: PartialTextProps, 14 | ): PartialAttribute< 15 | PartialTextORM, 16 | PartialTextControlType, 17 | string, 18 | FinalTextORM 19 | > { 20 | return { 21 | name: `text(${props ? JSON.stringify(props) : ""})`, 22 | orm: getPartialOrm(props), 23 | control: getPartialControl(props), 24 | finalize: function finalizeText() { 25 | return getFinalize(this) 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/text/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialStringControlType, 3 | PartialStringProps, 4 | } from "../string/index.js" 5 | 6 | export type PartialTextProps = 7 | PartialStringProps 8 | 9 | export type PartialTextControlType = 10 | PartialStringControlType 11 | 12 | export interface PartialTextORM { 13 | sequelize: { 14 | type: "TEXT" 15 | allowNull?: boolean 16 | primaryKey?: boolean 17 | defaultValue?: string | (() => string) | null 18 | unique?: boolean 19 | } 20 | } 21 | 22 | export interface FinalTextORM { 23 | sequelize: Required 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/constants.ts: -------------------------------------------------------------------------------- 1 | export const UUID_REGEX = 2 | /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ 3 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/finalizeControl.ts: -------------------------------------------------------------------------------- 1 | import type { PartialUuidControlType } from "./types.js" 2 | 3 | export function finalizeControl( 4 | props: Omit, "allowNullInfer">, 5 | ): Required, "allowNullInfer">> { 6 | // @ts-expect-error @todo HATCH-417 7 | delete props.allowNullInfer 8 | return { 9 | ...props, 10 | allowNull: props.allowNull !== false && !props.primary, 11 | min: props.min ?? 0, 12 | max: props.max ?? 255, 13 | primary: !!props.primary, 14 | default: props.default ?? null, 15 | regex: props.regex ?? /(.*?)/, 16 | readOnly: props.readOnly ?? false, 17 | ui: { 18 | displayName: props?.ui?.displayName ?? null, 19 | hidden: props?.ui?.hidden ?? false, 20 | enableCaseSensitiveContains: 21 | props?.ui?.enableCaseSensitiveContains ?? false, 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/finalizeOrm.ts: -------------------------------------------------------------------------------- 1 | import type { FinalUuidORM, PartialUuidORM } from "./types.js" 2 | 3 | export function finalizeOrm({ sequelize }: PartialUuidORM): FinalUuidORM { 4 | return { 5 | sequelize: { 6 | ...sequelize, 7 | allowNull: sequelize.allowNull !== false && !sequelize.primaryKey, 8 | primaryKey: !!sequelize.primaryKey, 9 | defaultValue: sequelize.defaultValue ?? null, 10 | unique: !!sequelize.unique || !!sequelize.primaryKey, 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/getFinalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { UUID_REGEX } from "./constants.js" 2 | 3 | import { uuid } from "./index.js" 4 | 5 | describe("getFinalize", () => { 6 | it("finalizes a partial attribute", () => { 7 | expect(uuid()).toEqual({ 8 | name: "uuid()", 9 | control: { 10 | allowNull: undefined, 11 | max: 36, 12 | min: 36, 13 | primary: undefined, 14 | regex: UUID_REGEX, 15 | type: "String", 16 | ui: { 17 | displayName: undefined, 18 | enableCaseSensitiveContains: undefined, 19 | hidden: undefined, 20 | }, 21 | }, 22 | orm: { 23 | sequelize: { 24 | allowNull: undefined, 25 | primaryKey: undefined, 26 | type: "UUID", 27 | }, 28 | }, 29 | finalize: expect.any(Function), 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/getPartialControl.ts: -------------------------------------------------------------------------------- 1 | import { UUID_REGEX } from "./constants.js" 2 | import type { PartialUuidControlType, PartialUuidProps } from "./types.js" 3 | 4 | export function getPartialControl( 5 | props?: PartialUuidProps, 6 | ): PartialUuidControlType { 7 | return { 8 | type: "String", 9 | readOnly: props?.readOnly, 10 | allowNull: props?.required == null ? props?.required : !props.required, 11 | allowNullInfer: (props?.required == null 12 | ? props?.required 13 | : !props.required) as TRequired extends true ? false : true, 14 | min: 36, 15 | max: 36, 16 | primary: props?.primary, 17 | default: props?.default, 18 | regex: UUID_REGEX, 19 | ui: { 20 | displayName: props?.ui?.displayName, 21 | hidden: props?.ui?.hidden, 22 | enableCaseSensitiveContains: props?.ui?.enableCaseSensitiveContains, 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/getPartialOrm.ts: -------------------------------------------------------------------------------- 1 | import type { PartialUuidORM, PartialUuidProps } from "./types.js" 2 | 3 | export function getPartialOrm( 4 | props?: PartialUuidProps, 5 | ): PartialUuidORM { 6 | return { 7 | sequelize: { 8 | type: "UUID", 9 | allowNull: props?.required == null ? props?.required : !props.required, 10 | primaryKey: props?.primary, 11 | defaultValue: props?.default, 12 | unique: props?.unique, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/index.ts: -------------------------------------------------------------------------------- 1 | import { getFinalize } from "./getFinalize.js" 2 | import { getPartialControl } from "./getPartialControl.js" 3 | import { getPartialOrm } from "./getPartialOrm.js" 4 | import type { 5 | FinalUuidORM, 6 | PartialUuidControlType, 7 | PartialUuidORM, 8 | PartialUuidProps, 9 | } from "./types.js" 10 | import type { PartialAttribute } from "../../types/index.js" 11 | 12 | export function uuid( 13 | props?: PartialUuidProps, 14 | ): PartialAttribute< 15 | PartialUuidORM, 16 | PartialUuidControlType, 17 | string, 18 | FinalUuidORM 19 | > { 20 | return { 21 | name: `uuid(${props ? JSON.stringify(props) : ""})`, 22 | orm: getPartialOrm(props), 23 | control: getPartialControl(props), 24 | finalize: function finalizeUuid() { 25 | return getFinalize(this) 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/dataTypes/uuid/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialDataTypeProps, 3 | PartialSequelizeDataType, 4 | } from "../../types/index.js" 5 | import type { PartialStringControlType } from "../string/index.js" 6 | 7 | export type PartialUuidProps = PartialDataTypeProps< 8 | string, 9 | TRequired 10 | > 11 | 12 | export type PartialUuidControlType = 13 | PartialStringControlType 14 | 15 | export interface PartialUuidORM { 16 | sequelize: Omit, "typeArgs"> 17 | } 18 | 19 | export interface FinalUuidORM { 20 | sequelize: Required< 21 | Omit, "typeArgs"> 22 | > 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/relationships/belongsTo/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { belongsTo } from "./index.js" 2 | 3 | describe("belongsTo", () => { 4 | it("belongsTo()", () => { 5 | expect(belongsTo()).toEqual({ 6 | type: "belongsTo", 7 | targetSchema: null, 8 | sourceAttribute: null, 9 | targetAttribute: null, 10 | }) 11 | }) 12 | 13 | it("belongsTo(schemaName)", () => { 14 | expect(belongsTo("SalesPerson")).toEqual({ 15 | type: "belongsTo", 16 | targetSchema: "SalesPerson", 17 | sourceAttribute: null, 18 | targetAttribute: null, 19 | }) 20 | }) 21 | 22 | it("belongsTo(schemaName, {sourceAttribute})", () => { 23 | expect(belongsTo("SalesPerson", { sourceAttribute: "finisherId" })).toEqual( 24 | { 25 | type: "belongsTo", 26 | targetSchema: "SalesPerson", 27 | sourceAttribute: "finisherId", 28 | targetAttribute: null, 29 | }, 30 | ) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/core/src/relationships/belongsTo/index.ts: -------------------------------------------------------------------------------- 1 | import type { PartialBelongsToRelationship } from "./types.js" 2 | 3 | // @todo HATCH-417 4 | export function belongsTo( 5 | targetSchema?: TTargetSchema, 6 | props?: { 7 | sourceAttribute: string 8 | targetAttribute?: string 9 | }, 10 | ): PartialBelongsToRelationship { 11 | return { 12 | type: "belongsTo", 13 | targetSchema: (targetSchema ?? null) as TTargetSchema, 14 | sourceAttribute: props?.sourceAttribute ?? null, 15 | targetAttribute: props?.targetAttribute ?? null, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/relationships/belongsTo/types.ts: -------------------------------------------------------------------------------- 1 | // @todo HATCH-417 2 | export interface PartialBelongsToRelationship< 3 | TTargetSchema extends string | undefined | null, 4 | > { 5 | type: "belongsTo" 6 | targetSchema: TTargetSchema 7 | sourceAttribute: string | null 8 | targetAttribute: string | null 9 | } 10 | 11 | export interface FinalBelongsToRelationship { 12 | type: "belongsTo" 13 | targetSchema: string 14 | sourceAttribute: string 15 | targetAttribute: string 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/relationships/hasMany/index.ts: -------------------------------------------------------------------------------- 1 | import type { PartialHasManyRelationship } from "./types.js" 2 | import { buildThrough } from "../hasManyThrough/buildThrough.js" 3 | 4 | // HATCH-417 5 | export function hasMany( 6 | schemaName?: TTargetSchema, 7 | props?: { 8 | targetAttribute: string 9 | sourceAttribute?: string 10 | }, 11 | ): PartialHasManyRelationship { 12 | const targetSchema = schemaName ?? null 13 | 14 | return { 15 | type: "hasMany", 16 | targetSchema: targetSchema as TTargetSchema, 17 | targetAttribute: props?.targetAttribute ?? null, 18 | sourceAttribute: props?.sourceAttribute ?? null, 19 | through: buildThrough(targetSchema as TTargetSchema), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/relationships/hasMany/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PartialHasManyThroughRelationship, 3 | ThroughOptions, 4 | } from "../hasManyThrough/types.js" 5 | 6 | // @todo HATCH-417 7 | export interface PartialHasManyRelationship< 8 | TTargetSchema extends string | undefined | null, 9 | > { 10 | type: "hasMany" 11 | targetSchema: TTargetSchema 12 | targetAttribute: string | null 13 | sourceAttribute: string | null 14 | through: ( 15 | through?: string | null, 16 | options?: ThroughOptions, 17 | ) => PartialHasManyThroughRelationship 18 | } 19 | 20 | export interface FinalHasManyRelationship { 21 | type: "hasMany" 22 | targetSchema: string 23 | targetAttribute: string 24 | sourceAttribute: string 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/relationships/hasOne/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { hasOne } from "./index.js" 2 | 3 | describe("hasOne", () => { 4 | it("hasOne()", () => { 5 | expect(hasOne()).toEqual({ 6 | type: "hasOne", 7 | targetSchema: null, 8 | targetAttribute: null, 9 | sourceAttribute: null, 10 | }) 11 | }) 12 | 13 | it("hasOne(schemaName)", () => { 14 | expect(hasOne("SalesPerson")).toEqual({ 15 | type: "hasOne", 16 | targetSchema: "SalesPerson", 17 | targetAttribute: null, 18 | sourceAttribute: null, 19 | }) 20 | }) 21 | 22 | it("hasOne(schemaName, {targetAttribute})", () => { 23 | expect(hasOne("SalesPerson", { targetAttribute: "salesId" })).toEqual({ 24 | type: "hasOne", 25 | targetSchema: "SalesPerson", 26 | targetAttribute: "salesId", 27 | sourceAttribute: null, 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/core/src/relationships/hasOne/index.ts: -------------------------------------------------------------------------------- 1 | import type { PartialHasOneRelationship } from "./types.js" 2 | 3 | // @todo HATCH-417 4 | export function hasOne( 5 | targetSchema?: TTargetSchema, 6 | props?: { 7 | targetAttribute: string 8 | sourceAttribute?: string 9 | }, 10 | ): PartialHasOneRelationship { 11 | return { 12 | type: "hasOne", 13 | targetSchema: (targetSchema ?? null) as TTargetSchema, 14 | targetAttribute: props?.targetAttribute ?? null, 15 | sourceAttribute: props?.sourceAttribute ?? null, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/relationships/hasOne/types.ts: -------------------------------------------------------------------------------- 1 | // @todo HATCH-417 2 | export interface PartialHasOneRelationship< 3 | TTargetSchema extends string | undefined | null, 4 | > { 5 | type: "hasOne" 6 | targetSchema: TTargetSchema 7 | targetAttribute: string | null 8 | sourceAttribute: string | null 9 | } 10 | 11 | export interface FinalHasOneRelationship { 12 | type: "hasOne" 13 | targetSchema: string 14 | targetAttribute: string 15 | sourceAttribute: string 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/relationships/index.ts: -------------------------------------------------------------------------------- 1 | export { belongsTo } from "./belongsTo/index.js" 2 | export { hasMany } from "./hasMany/index.js" 3 | export { hasOne } from "./hasOne/index.js" 4 | 5 | export type { 6 | PartialBelongsToRelationship, 7 | FinalBelongsToRelationship, 8 | } from "./belongsTo/types.js" 9 | export type { 10 | PartialHasManyRelationship, 11 | FinalHasManyRelationship, 12 | } from "./hasMany/types.js" 13 | export type { 14 | PartialHasManyThroughRelationship, 15 | FinalHasManyThroughRelationship, 16 | ThroughOptions, 17 | } from "./hasManyThrough/types.js" 18 | export type { 19 | PartialHasOneRelationship, 20 | FinalHasOneRelationship, 21 | } from "./hasOne/types.js" 22 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./schema.js" 2 | -------------------------------------------------------------------------------- /packages/core/src/util/camelCaseToPascalCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { camelCaseToPascalCase } from "./camelCaseToPascalCase.js" 2 | 3 | describe("camelCaseToPascalCase", () => { 4 | it("works as expected", () => { 5 | expect(camelCaseToPascalCase("user")).toBe("User") 6 | expect(camelCaseToPascalCase("userName")).toBe("UserName") 7 | expect(camelCaseToPascalCase("parent1LastName")).toBe("Parent1LastName") 8 | expect(camelCaseToPascalCase("addressLine1")).toBe("AddressLine1") 9 | expect(camelCaseToPascalCase("u")).toBe("U") 10 | expect(camelCaseToPascalCase("")).toBe("") 11 | expect(camelCaseToPascalCase(null as unknown as string)).toBeNull() 12 | expect( 13 | camelCaseToPascalCase(undefined as unknown as string), 14 | ).toBeUndefined() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/core/src/util/camelCaseToPascalCase.ts: -------------------------------------------------------------------------------- 1 | export function camelCaseToPascalCase(camelCaseString: string): string { 2 | return typeof camelCaseString !== "string" 3 | ? camelCaseString 4 | : camelCaseString.charAt(0).toUpperCase() + camelCaseString.slice(1) 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/util/camelCaseToTitleCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { camelCaseToTitleCase } from "./camelCaseToTitleCase.js" 2 | 3 | describe("camelCaseToTitleCase", () => { 4 | it("works as expected", () => { 5 | expect(camelCaseToTitleCase("user")).toBe("User") 6 | expect(camelCaseToTitleCase("userName")).toBe("User Name") 7 | expect(camelCaseToTitleCase("parent1LastName")).toBe("Parent 1 Last Name") 8 | expect(camelCaseToTitleCase("addressLine1")).toBe("Address Line 1") 9 | expect(camelCaseToTitleCase("zipCode12345")).toBe("Zip Code 12345") 10 | expect(camelCaseToTitleCase("u")).toBe("U") 11 | expect(camelCaseToTitleCase("")).toBe("") 12 | expect(camelCaseToTitleCase(null as unknown as string)).toBeNull() 13 | expect(camelCaseToTitleCase(undefined as unknown as string)).toBeUndefined() 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/src/util/camelCaseToTitleCase.ts: -------------------------------------------------------------------------------- 1 | export function camelCaseToTitleCase( 2 | camelCaseString?: string, 3 | ): string | undefined { 4 | return typeof camelCaseString !== "string" 5 | ? camelCaseString 6 | : camelCaseString 7 | ?.replace(/([A-Z]|[0-9]+)/g, " $1") 8 | .replace(/^./, function (str) { 9 | return str.toUpperCase() 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/util/getEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { pascalCaseToKebabCase } from "./pascalCaseToKebabCase.js" 2 | import { pluralize } from "./pluralize.js" 3 | import type { FinalSchema } from "../types/index.js" 4 | 5 | export function getEndpoint( 6 | schema: Pick, 7 | ): string | undefined { 8 | const { name, namespace } = schema 9 | const pluralName = pascalCaseToKebabCase(schema.pluralName ?? pluralize(name)) 10 | return namespace 11 | ? `${pascalCaseToKebabCase(namespace)}/${pluralName}` 12 | : pluralName 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/util/getSchemaKey.spec.ts: -------------------------------------------------------------------------------- 1 | import { getSchemaKey } from "./getSchemaKey.js" 2 | 3 | describe("getSchemaKey", () => { 4 | it("works as expected", () => { 5 | expect(getSchemaKey({ name: "Two" })).toBe("Two") 6 | expect(getSchemaKey({ namespace: "One", name: "Two" })).toBe("One_Two") 7 | expect(getSchemaKey({ name: undefined as unknown as string })).toBe("") 8 | expect( 9 | getSchemaKey({ 10 | namespace: "One", 11 | name: undefined as unknown as string, 12 | }), 13 | ).toBe("One_") 14 | expect( 15 | getSchemaKey(null as unknown as { namespace: string; name: string }), 16 | ).toBeNull() 17 | expect( 18 | getSchemaKey(undefined as unknown as { namespace: string; name: string }), 19 | ).toBeUndefined() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/core/src/util/getSchemaKey.ts: -------------------------------------------------------------------------------- 1 | import type { FinalSchema } from "../types/index.js" 2 | 3 | export function getSchemaKey( 4 | schema: Pick, 5 | ): string { 6 | if (!schema) return schema 7 | 8 | const { name, namespace } = schema 9 | return !namespace ? name ?? "" : `${namespace}_${name ?? ""}` 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/util/pascalCaseToCamelCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { pascalCaseToCamelCase } from "./pascalCaseToCamelCase.js" 2 | 3 | describe("pascalCaseToCamelCase", () => { 4 | it("works as expected", () => { 5 | expect(pascalCaseToCamelCase("User")).toBe("user") 6 | expect(pascalCaseToCamelCase("UserName")).toBe("userName") 7 | expect(pascalCaseToCamelCase("Parent1LastName")).toBe("parent1LastName") 8 | expect(pascalCaseToCamelCase("AddressLine1")).toBe("addressLine1") 9 | expect(pascalCaseToCamelCase("U")).toBe("u") 10 | expect(pascalCaseToCamelCase("")).toBe("") 11 | expect(pascalCaseToCamelCase(null as unknown as string)).toBeNull() 12 | expect( 13 | pascalCaseToCamelCase(undefined as unknown as string), 14 | ).toBeUndefined() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/core/src/util/pascalCaseToCamelCase.ts: -------------------------------------------------------------------------------- 1 | export function pascalCaseToCamelCase(pascalCaseString: string): string { 2 | return typeof pascalCaseString !== "string" 3 | ? pascalCaseString 4 | : pascalCaseString.charAt(0).toLowerCase() + pascalCaseString.slice(1) 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/util/pascalCaseToKebabCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { pascalCaseToKebabCase } from "./pascalCaseToKebabCase.js" 2 | 3 | describe("pascalCaseToKebabCase", () => { 4 | it("works as expected", () => { 5 | expect(pascalCaseToKebabCase("User")).toBe("user") 6 | expect(pascalCaseToKebabCase("UserName")).toBe("user-name") 7 | expect(pascalCaseToKebabCase("Parent1LastName")).toBe("parent1-last-name") 8 | expect(pascalCaseToKebabCase("AddressLine1")).toBe("address-line1") 9 | expect(pascalCaseToKebabCase("U")).toBe("u") 10 | expect(pascalCaseToKebabCase("")).toBe("") 11 | expect(pascalCaseToKebabCase(null as unknown as string)).toBeNull() 12 | expect( 13 | pascalCaseToKebabCase(undefined as unknown as string), 14 | ).toBeUndefined() 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/core/src/util/pascalCaseToKebabCase.ts: -------------------------------------------------------------------------------- 1 | export function pascalCaseToKebabCase( 2 | pascalCaseString?: string, 3 | ): string | undefined { 4 | return typeof pascalCaseString !== "string" 5 | ? pascalCaseString 6 | : pascalCaseString 7 | ?.replace( 8 | /[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, 9 | (match) => "-" + match.toLowerCase(), 10 | ) 11 | .substring(1) 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/util/pluralize.spec.ts: -------------------------------------------------------------------------------- 1 | import { pluralize } from "./pluralize.js" 2 | 3 | describe("pluralize", () => { 4 | it("should pluralize a word with an s", () => { 5 | expect(pluralize("word")).toBe("words") 6 | expect(pluralize("words")).toBe("wordss") 7 | expect(pluralize("twoWord")).toBe("twoWords") 8 | expect(pluralize(null as unknown as string)).toBeNull() 9 | expect(pluralize(undefined as unknown as string)).toBeUndefined() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/core/src/util/pluralize.ts: -------------------------------------------------------------------------------- 1 | export function pluralize(singularName: string): string { 2 | if (typeof singularName !== "string") { 3 | return singularName 4 | } 5 | 6 | return singularName + "s" 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/util/singularize.spec.ts: -------------------------------------------------------------------------------- 1 | import { singularize } from "./singularize.js" 2 | 3 | describe("singularize", () => { 4 | it("removes trailing 's'", () => { 5 | expect(singularize("users")).toBe("user") 6 | expect(singularize("persons")).toBe("person") 7 | expect(singularize("s")).toBe("") 8 | }) 9 | 10 | it("ignores if there is no trailing 's'", () => { 11 | expect(singularize("")).toBe("") 12 | expect(singularize("user")).toBe("user") 13 | expect(singularize("u")).toBe("u") 14 | expect(singularize(null as unknown as string)).toBeNull() 15 | expect(singularize(undefined as unknown as string)).toBeUndefined() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/util/singularize.ts: -------------------------------------------------------------------------------- 1 | export function singularize(pluralName: string): string { 2 | if (typeof pluralName !== "string") { 3 | return pluralName 4 | } 5 | 6 | return pluralName.endsWith("s") 7 | ? pluralName.substring(0, pluralName.length - 1) 8 | : pluralName 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/util/uuidv4.ts: -------------------------------------------------------------------------------- 1 | export * from "@hatchifyjs/crypto" 2 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "rootDir": "./src" 5 | }, 6 | "extends": "../../tsconfig.json", 7 | "include": [ 8 | "src", 9 | "src/core.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/create/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignore-dirs: 2 | - /dist 3 | ignores: 4 | - "@hatchifyjs/core" 5 | - "@hatchifyjs/express" 6 | - "@hatchifyjs/koa" 7 | - "@hatchifyjs/react" 8 | - "@mui/material" 9 | - "dotenv" 10 | - "express" 11 | - "koa" 12 | - "koa-connect" 13 | - "react" 14 | - "react-dom" 15 | - "vite" 16 | -------------------------------------------------------------------------------- /packages/create/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from "unbuild" 2 | 3 | export default defineBuildConfig({ 4 | entries: ["src/index"], 5 | clean: true, 6 | rollup: { 7 | inlineDependencies: true, 8 | esbuild: { 9 | target: "node18", 10 | minify: true, 11 | }, 12 | }, 13 | alias: { 14 | // we can always use non-transpiled code since we support node 18+ 15 | prompts: "prompts/lib/index.js", 16 | }, 17 | hooks: {}, 18 | }) 19 | -------------------------------------------------------------------------------- /packages/create/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "./dist/index.mjs" 4 | -------------------------------------------------------------------------------- /packages/create/src/types.ts: -------------------------------------------------------------------------------- 1 | type ColorFunc = (str: string | number) => string 2 | 3 | export type Backend = { 4 | name: string 5 | display: string 6 | color: ColorFunc 7 | dependencies: string[] 8 | devDependencies: string[] 9 | } 10 | 11 | export type Database = { 12 | name: string 13 | display: string 14 | color: ColorFunc 15 | dependencies: string[] 16 | devDependencies: string[] 17 | } 18 | 19 | export type Frontend = { 20 | name: string 21 | display: string 22 | color: ColorFunc 23 | dependencies: string[] 24 | devDependencies: string[] 25 | } 26 | -------------------------------------------------------------------------------- /packages/create/template-express/_gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/create/template-express/schemas.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/create/template-express/schemas.ts -------------------------------------------------------------------------------- /packages/create/template-koa/_gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/create/template-koa/schemas.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/create/template-koa/schemas.ts -------------------------------------------------------------------------------- /packages/create/template-react/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import App from "./App.js" 4 | import "./index.css" 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /packages/create/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["build.config.ts", "src"], 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "ES2022", 6 | "module": "ES2020", 7 | "moduleResolution": "bundler", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "declaration": false, 11 | "sourceMap": false, 12 | "noUnusedLocals": true, 13 | "esModuleInterop": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/crypto/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: 2 | - "@hatchifyjs/core" 3 | -------------------------------------------------------------------------------- /packages/crypto/README.md: -------------------------------------------------------------------------------- 1 | Isomorphic WebCrypto package. Only Isomorphic. No Polyfills. Used by Hatchify to create UUIDs. -------------------------------------------------------------------------------- /packages/crypto/index.browser.js: -------------------------------------------------------------------------------- 1 | export function getCrypto() { 2 | return window.crypto 3 | } 4 | 5 | export function uuidv4() { 6 | return window.crypto.randomUUID() 7 | } 8 | -------------------------------------------------------------------------------- /packages/crypto/index.browser.spec.js: -------------------------------------------------------------------------------- 1 | describe("getCrypto", () => { 2 | it("returns window.crypto if available", async () => { 3 | Object.defineProperty(window, "crypto", { 4 | value: "window.crypto", 5 | }) 6 | const { getCrypto } = await import("@hatchifyjs/core/dist/util/uuidv4.js") 7 | expect(getCrypto()).toBe("window.crypto") 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/crypto/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@hatchifyjs/crypto" 2 | 3 | export declare function getCrypto(): Crypto 4 | 5 | export declare function uuidv4(): string 6 | -------------------------------------------------------------------------------- /packages/crypto/index.js: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto" 2 | 3 | export function getCrypto() { 4 | return crypto.webcrypto 5 | } 6 | 7 | export function uuidv4() { 8 | return crypto.webcrypto.randomUUID() 9 | } 10 | -------------------------------------------------------------------------------- /packages/crypto/index.spec.js: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto" 2 | 3 | import { getCrypto, uuidv4 } from "./index.js" 4 | 5 | describe("crypto", () => { 6 | it("generates a uuid", () => { 7 | expect(uuidv4()).toMatch( 8 | /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, 9 | ) 10 | }) 11 | }) 12 | 13 | describe("getCrypto", () => { 14 | it("returns node crypto if available", async () => { 15 | expect(getCrypto()).toBe(crypto.webcrypto) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/crypto/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverageFrom": [ 3 | "/**/*.js", 4 | "!/**/*.browser.js", 5 | "!/**/*.spec.js", 6 | "!/**/coverage/**/*.js" 7 | ], 8 | "coverageDirectory": "../../coverage/crypto", 9 | "coverageReporters": ["html", "text", "text-summary"], 10 | "coverageThreshold": { 11 | "global": { 12 | "branches": 100, 13 | "functions": 100, 14 | "lines": 100, 15 | "statements": 100 16 | } 17 | }, 18 | "roots": [""], 19 | "testEnvironment": "node", 20 | "testPathIgnorePatterns": [".browser"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/design-mui/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@emotion/styled", "@testing-library/jest-dom", "@vitest/coverage-c8", "lodash"] 2 | -------------------------------------------------------------------------------- /packages/design-mui/README.md: -------------------------------------------------------------------------------- 1 | # @hatchifyjs/design-mui 2 | 3 | Material UI implementation on of Hatchify's React components. This isn't used directly. Use [`@hatchifyjs/react`](https://github.com/bitovi/hatchify/blob/main/docs/react/README.md) instead. 4 | -------------------------------------------------------------------------------- /packages/design-mui/src/assets/index.ts: -------------------------------------------------------------------------------- 1 | export { Robochicken } from "./Robochicken.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiAutocomplete/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from "./MuiAutocomplete.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiDataGrid/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MuiDataGrid } from "./MuiDataGrid.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiEverything/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MuiEverything } from "./MuiEverything.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiFilters/components/inputs/ColumnSelect.tsx: -------------------------------------------------------------------------------- 1 | import { InputLabel, MenuItem, Select } from "@mui/material" 2 | import { camelCaseToTitleCase } from "@hatchifyjs/core" 3 | 4 | const ColumnSelect: React.FC<{ 5 | fields: string[] 6 | labelId: string 7 | value: string 8 | onChange: (value: string) => void 9 | }> = ({ value, labelId, fields, onChange }) => { 10 | return ( 11 | <> 12 | Column 13 | 27 | 28 | ) 29 | } 30 | 31 | export default ColumnSelect 32 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiFilters/components/inputs/DateInput.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, vi } from "vitest" 2 | import { render } from "@testing-library/react" 3 | import DateInput from "./DateInput.js" 4 | 5 | describe("components/MuiFilters/inputs/DateInput", () => { 6 | it("works", async () => { 7 | render( 8 | , 9 | ) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiFilters/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MuiFilters } from "./MuiFilters.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiFilters/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiLayout/MuiLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest" 2 | import { render, screen } from "@testing-library/react" 3 | import { string } from "@hatchifyjs/core" 4 | import { MuiLayout } from "./MuiLayout.js" 5 | 6 | const partialSchemas = { 7 | Todo: { 8 | name: "Todo", 9 | attributes: { name: string() }, 10 | }, 11 | } 12 | 13 | describe("hatchifyjs/presentation/mui/MuiLayout", () => { 14 | describe("MuiLayout", () => { 15 | it("works", async () => { 16 | render() 17 | expect(await screen.findByText("Todo")).toBeDefined() 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { MuiLayout } from "./MuiLayout.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiList/components/MuiBodySkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton, TableCell, TableRow } from "@mui/material" 2 | import type { HatchifyColumn } from "@hatchifyjs/react-ui" 3 | 4 | export const MuiBodySkeleton: React.FC<{ columns: HatchifyColumn[] }> = ({ 5 | columns, 6 | }) => { 7 | return ( 8 | <> 9 | {[1, 2, 3].map((key) => ( 10 | 11 | {columns.map((column) => ( 12 | 13 | 14 | 15 | ))} 16 | 17 | ))} 18 | 19 | ) 20 | } 21 | 22 | export default MuiBodySkeleton 23 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiList/components/MuiHeaders/components/Sortable.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from "@hatchifyjs/rest-client" 2 | import { Sort } from "./Sort.js" 3 | import type { HatchifyColumn, HeaderProps } from "@hatchifyjs/react-ui" 4 | 5 | export const Sortable: React.FC< 6 | Pick & { 7 | children: React.ReactNode 8 | isPending: Meta["isPending"] 9 | columnKey: HatchifyColumn["key"] 10 | sortable: boolean 11 | } 12 | > = ({ 13 | children, 14 | columnKey, 15 | direction, 16 | isPending, 17 | setSort, 18 | sortable, 19 | sortBy, 20 | }) => 21 | sortable ? ( 22 | 29 | {children} 30 | 31 | ) : ( 32 | <>{children} 33 | ) 34 | 35 | export default Sortable 36 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiList/components/MuiHeaders/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Sort } from "./Sort.js" 2 | export { default as Sortable } from "./Sortable.js" 3 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiList/components/MuiHeaders/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./MuiHeaders.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiList/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MuiBody } from "./MuiBody.js" 2 | export { default as MuiBodySkeleton } from "./MuiBodySkeleton.js" 3 | export { default as MuiHeaders } from "./MuiHeaders/index.js" 4 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiList/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MuiList.js" 2 | export * from "./components/index.js" 3 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiNavigation/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MuiNavigation } from "./MuiNavigation.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiNoSchemas/MuiNoSchemas.tsx: -------------------------------------------------------------------------------- 1 | import { Grid, Typography } from "@mui/material" 2 | import { Robochicken } from "../../assets" 3 | 4 | function MuiNoSchemas(): JSX.Element { 5 | return ( 6 | 7 | 8 | 14 | Welcome to Hatchify! 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | MuiNoSchemas.displayName = "NoSchemas" 25 | 26 | export default MuiNoSchemas 27 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiNoSchemas/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MuiNoSchemas } from "./MuiNoSchemas.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiPagination/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MuiPagination } from "./MuiPagination.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiProvider/DefaultDisplayComponents/DefaultDisplayComponents.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import { Chip } from "@mui/material" 3 | import { css } from "@emotion/react" 4 | 5 | import type { Relationship as RelationshipType } from "@hatchifyjs/react-ui" 6 | 7 | const styles = { 8 | chip: css` 9 | margin: 0 2.5px; 10 | &:first-of-type { 11 | margin-left: 0; 12 | } 13 | &:last-of-type { 14 | margin-right: 0; 15 | } 16 | `, 17 | } 18 | 19 | export const Relationship: React.FC<{ value: RelationshipType }> = ({ 20 | value, 21 | }) => 22 | 23 | export const RelationshipList: React.FC<{ values: RelationshipType[] }> = ({ 24 | values, 25 | }) => ( 26 | <> 27 | {values.map((value) => ( 28 | 29 | ))} 30 | 31 | ) 32 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiProvider/DefaultDisplayComponents/index.ts: -------------------------------------------------------------------------------- 1 | export { Relationship, RelationshipList } from "./DefaultDisplayComponents.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiProvider/DefaultFieldComponents/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | String, 3 | Number, 4 | Boolean, 5 | Date, 6 | Relationship, 7 | } from "./DefaultFieldComponents.js" 8 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/MuiProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { MuiProvider } from "./MuiProvider.js" 2 | -------------------------------------------------------------------------------- /packages/design-mui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MuiProvider/index.js" 2 | export * from "./MuiDataGrid/index.js" 3 | export * from "./MuiEverything/index.js" 4 | export * from "./MuiFilters/index.js" 5 | export * from "./MuiLayout/index.js" 6 | export * from "./MuiList/index.js" 7 | export * from "./MuiPagination/index.js" 8 | -------------------------------------------------------------------------------- /packages/design-mui/src/design-mui.ts: -------------------------------------------------------------------------------- 1 | import { MuiProvider } from "./components/index.js" 2 | 3 | export { 4 | MuiDataGrid as DataGrid, 5 | MuiList as List, 6 | MuiPagination as Pagination, 7 | MuiFilters as Filters, 8 | } from "./components/index.js" 9 | 10 | export default MuiProvider 11 | -------------------------------------------------------------------------------- /packages/design-mui/src/services/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/design-mui/src/services/index.ts -------------------------------------------------------------------------------- /packages/design-mui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "bundler", // uses "bundler" as this library is currently built with vite 5 | "jsx": "react-jsx", 6 | "outDir": "dist" 7 | }, 8 | "extends": "../../tsconfig.json", 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/design-mui/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite" 3 | import dts from "vite-plugin-dts" 4 | import react from "@vitejs/plugin-react" 5 | import nodeExternals from "rollup-plugin-node-externals" 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | build: { 10 | lib: { 11 | entry: "src/design-mui.ts", 12 | formats: ["es"], 13 | }, 14 | }, 15 | esbuild: { 16 | supported: { 17 | "top-level-await": true, 18 | }, 19 | }, 20 | plugins: [{ ...nodeExternals(), enforce: "pre" }, dts(), react()], 21 | test: { 22 | globals: true, 23 | environment: "jsdom", 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /packages/express/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignore-dirs: 2 | - /dist 3 | ignores: 4 | - "@types/*" 5 | - json-api-serializer 6 | - pg 7 | - ts-jest 8 | -------------------------------------------------------------------------------- /packages/express/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | docs 3 | -------------------------------------------------------------------------------- /packages/express/.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | docs 4 | -------------------------------------------------------------------------------- /packages/express/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverageFrom": ["/src/**/*.ts"], 3 | "coverageDirectory": "../../coverage/express", 4 | "coverageReporters": ["html", "text", "text-summary"], 5 | "coverageThreshold": { 6 | "global": { 7 | "branches": 76, 8 | "functions": 75, 9 | "lines": 87, 10 | "statements": 84 11 | } 12 | }, 13 | "extensionsToTreatAsEsm": [".ts"], 14 | "moduleNameMapper": { 15 | "^(\\.{1,2}/.*)\\.js$": "$1" 16 | }, 17 | "modulePathIgnorePatterns": ["/src/testing"], 18 | "roots": ["/src"], 19 | "testEnvironment": "node", 20 | "transform": { 21 | ".+\\.ts$": [ 22 | "ts-jest", 23 | { 24 | "useESM": true 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "NodeNext", 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "strict": true, 9 | "strictNullChecks": true 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/koa/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignore-dirs: 2 | - /dist 3 | ignores: 4 | - "@types/*" 5 | - json-api-serializer 6 | - pg 7 | - ts-jest 8 | -------------------------------------------------------------------------------- /packages/koa/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | docs 3 | -------------------------------------------------------------------------------- /packages/koa/.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | docs 4 | -------------------------------------------------------------------------------- /packages/koa/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverageFrom": ["/src/**/*.ts"], 3 | "coverageDirectory": "../../coverage/koa", 4 | "coverageReporters": ["html", "text", "text-summary"], 5 | "coverageThreshold": { 6 | "global": { 7 | "branches": 91, 8 | "functions": 94, 9 | "lines": 91, 10 | "statements": 91 11 | } 12 | }, 13 | "extensionsToTreatAsEsm": [".ts"], 14 | "moduleNameMapper": { 15 | "^(\\.{1,2}/.*)\\.js$": "$1" 16 | }, 17 | "modulePathIgnorePatterns": ["/src/testing"], 18 | "roots": ["/src"], 19 | "testEnvironment": "node", 20 | "transform": { 21 | ".+\\.ts$": [ 22 | "ts-jest", 23 | { 24 | "useESM": true 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/koa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "NodeNext", 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolvePackageJsonImports": true 11 | }, 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/koa/typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["./src/exports.ts"], 4 | "out": "docs", 5 | "theme": "default", 6 | "name": "@hatchifyjs/koa", 7 | "includeVersion": true, 8 | "disableSources": true, 9 | "githubPages": true, 10 | "media": "./media", 11 | "navigationLinks": { 12 | "Bitovi": "https://bitovi.com", 13 | "GitHub": "https://github.com/bitovi/hatchify" 14 | }, 15 | "sidebarLinks": { 16 | "Confluence": "https://bitovi.atlassian.net/wiki/spaces/SCAFFOLD/overview", 17 | "Jira Software": "https://bitovi.atlassian.net/browse/SCAFFOLD" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/node/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignore-dirs: 2 | - /dist 3 | ignores: 4 | - "@types/*" 5 | - ts-jest 6 | -------------------------------------------------------------------------------- /packages/node/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | -------------------------------------------------------------------------------- /packages/node/.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | -------------------------------------------------------------------------------- /packages/node/README.md: -------------------------------------------------------------------------------- 1 | Core NodeJS functionality for Hatchify. This isn't used directly. Instead use [@hatchifyjs/express](https://github.com/bitovi/hatchify/blob/main/docs/express/README.md) or [@hatchifyjs/koa](https://github.com/bitovi/hatchify/blob/main/docs/koa/README.md). -------------------------------------------------------------------------------- /packages/node/jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverageFrom": ["/src/**/*.ts"], 3 | "coverageDirectory": "../../coverage/node", 4 | "coverageReporters": ["html", "text", "text-summary"], 5 | "coverageThreshold": { 6 | "global": { 7 | "branches": 57, 8 | "functions": 55, 9 | "lines": 57, 10 | "statements": 57 11 | } 12 | }, 13 | "extensionsToTreatAsEsm": [".ts"], 14 | "moduleNameMapper": { 15 | "^(\\.{1,2}/.*)\\.js$": "$1" 16 | }, 17 | "roots": ["/src"], 18 | "setupFilesAfterEnv": ["./src/jest.setup.ts"], 19 | "testEnvironment": "node", 20 | "transform": { 21 | ".+\\.ts$": [ 22 | "ts-jest", 23 | { 24 | "useESM": true 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/node/src/error/constants.ts: -------------------------------------------------------------------------------- 1 | export enum codes { 2 | ERR_CONFLICT = "resource-conflict-occurred", 3 | ERR_INVALID_PARAMETER = "invalid-parameter", 4 | ERR_NOT_FOUND = "not-found", 5 | ERR_DATABASE_ERROR = "database-error", 6 | ERR_SERVER_ERROR = "server-error", 7 | ERR_VALUE_REQUIRED = "value-required", 8 | ERR_UNEXPECTED_VALUE = "unexpected-value", 9 | ERR_RELATIONSHIP_PATH = "relationship-path", 10 | } 11 | 12 | export enum statusCodes { 13 | BAD_REQUEST = 400, 14 | NOT_FOUND = 404, 15 | CONFLICT = 409, 16 | UNPROCESSABLE_ENTITY = 422, 17 | INTERNAL_SERVER_ERROR = 500, 18 | } 19 | -------------------------------------------------------------------------------- /packages/node/src/error/handlers/hatchifyErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyError } from "../types/index.js" 2 | import type { HatchifyErrorOptions } from "../types/index.js" 3 | 4 | export function hatchifyErrorHandler( 5 | error: HatchifyErrorOptions, 6 | ): HatchifyError { 7 | console.error("Uncaught Hatchify Error:", error) 8 | 9 | if (error instanceof HatchifyError) { 10 | return error 11 | } 12 | 13 | return new HatchifyError(error) 14 | } 15 | -------------------------------------------------------------------------------- /packages/node/src/error/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./errorResponseHandler.js" 2 | -------------------------------------------------------------------------------- /packages/node/src/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants.js" 2 | export * from "./handlers/index.js" 3 | export * from "./types/index.js" 4 | -------------------------------------------------------------------------------- /packages/node/src/error/types/ConflictError.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyError } from "./HatchifyError.js" 2 | import { codes, statusCodes } from "../constants.js" 3 | 4 | export class ConflictError extends HatchifyError { 5 | constructor({ pointer }: { pointer: string }) { 6 | super({ 7 | title: "Conflict", 8 | code: codes.ERR_CONFLICT, 9 | status: statusCodes.CONFLICT, 10 | pointer, 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/node/src/error/types/NotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyError } from "./HatchifyError.js" 2 | import type { HatchifyErrorOptions } from "./HatchifyError.js" 3 | import { codes, statusCodes } from "../constants.js" 4 | 5 | export class NotFoundError extends HatchifyError { 6 | constructor({ 7 | detail, 8 | parameter, 9 | pointer, 10 | }: Pick) { 11 | super({ 12 | code: codes.ERR_NOT_FOUND, 13 | status: statusCodes.NOT_FOUND, 14 | title: "Resource not found.", 15 | detail, 16 | parameter, 17 | pointer, 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/node/src/error/types/RelationshipPathError.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyError } from "./HatchifyError.js" 2 | import { codes, statusCodes } from "../constants.js" 3 | 4 | export class RelationshipPathError extends HatchifyError { 5 | constructor({ 6 | detail, 7 | parameter, 8 | pointer, 9 | }: { 10 | detail?: string 11 | parameter?: string 12 | pointer?: string 13 | } = {}) { 14 | super({ 15 | status: statusCodes.BAD_REQUEST, 16 | code: codes.ERR_RELATIONSHIP_PATH, 17 | title: "Relationship path could not be identified.", 18 | detail, 19 | parameter, 20 | pointer, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/node/src/error/types/UnexpectedValueError.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyError } from "./HatchifyError.js" 2 | import { codes, statusCodes } from "../constants.js" 3 | 4 | export class UnexpectedValueError extends HatchifyError { 5 | constructor({ 6 | detail, 7 | parameter, 8 | pointer, 9 | }: { 10 | detail?: string 11 | parameter?: string 12 | pointer?: string 13 | }) { 14 | super({ 15 | status: statusCodes.UNPROCESSABLE_ENTITY, 16 | code: codes.ERR_UNEXPECTED_VALUE, 17 | title: "Unexpected value.", 18 | detail, 19 | parameter, 20 | pointer, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/node/src/error/types/ValueRequiredError.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyError } from "./HatchifyError.js" 2 | import type { HatchifyErrorOptions } from "./HatchifyError.js" 3 | import { codes, statusCodes } from "../constants.js" 4 | 5 | export class ValueRequiredError extends HatchifyError { 6 | constructor({ 7 | detail, 8 | parameter, 9 | pointer, 10 | }: Pick) { 11 | super({ 12 | status: statusCodes.UNPROCESSABLE_ENTITY, 13 | code: codes.ERR_VALUE_REQUIRED, 14 | title: "Payload is missing a required value.", 15 | detail, 16 | parameter, 17 | pointer, 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/node/src/error/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ConflictError.js" 2 | export * from "./HatchifyError.js" 3 | export * from "./NotFoundError.js" 4 | export * from "./RelationshipPathError.js" 5 | export * from "./UnexpectedValueError.js" 6 | export * from "./ValueRequiredError.js" 7 | -------------------------------------------------------------------------------- /packages/node/src/jest.d.ts: -------------------------------------------------------------------------------- 1 | type OwnMatcher = ( 2 | this: jest.MatcherContext, 3 | actual: Error[], 4 | ...params: Params 5 | ) => jest.CustomMatcherResult 6 | 7 | declare global { 8 | namespace jest { 9 | interface ExpectExtendMap { 10 | // A helper for comparing errors array. `toEqual` always returns `true` 11 | toEqualErrors: OwnMatcher<[errors: Error[]]> 12 | } 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 15 | interface Matchers { 16 | toEqualErrors(errors: Error[]): T 17 | } 18 | } 19 | } 20 | 21 | export {} 22 | -------------------------------------------------------------------------------- /packages/node/src/jest.setup.ts: -------------------------------------------------------------------------------- 1 | function toPojo(error: Error) { 2 | return Object.entries(error).reduce( 3 | (acc, [key, value]) => ({ ...acc, [key]: value }), 4 | {}, 5 | ) 6 | } 7 | 8 | expect.extend({ 9 | toEqualErrors(actual, expected) { 10 | const actualPojos = actual.map(toPojo) 11 | const expectedPojos = expected.map(toPojo) 12 | const stringifiedActual = JSON.stringify(actualPojos) 13 | const stringifiedExpected = JSON.stringify(expectedPojos) 14 | const pass = stringifiedActual === stringifiedExpected 15 | 16 | return { 17 | pass, 18 | message: pass 19 | ? () => 20 | `expected ${stringifiedActual} not to equal ${stringifiedExpected}` 21 | : () => `expected ${stringifiedActual} to equal ${stringifiedExpected}`, 22 | } 23 | }, 24 | }) 25 | 26 | export {} 27 | -------------------------------------------------------------------------------- /packages/node/src/parse/IsValidInclude.ts: -------------------------------------------------------------------------------- 1 | import type { FinalSchema } from "@hatchifyjs/core" 2 | 3 | import type { ModelFunctionsCollection } from "../types.js" 4 | 5 | export function isValidInclude( 6 | schemaName: string, 7 | include: string[], 8 | allSchemas: ModelFunctionsCollection, 9 | ): boolean { 10 | if (!allSchemas[schemaName]) { 11 | return false 12 | } 13 | 14 | if (include.length === 0) { 15 | return false 16 | } 17 | 18 | if (include.length === 1) { 19 | return !!allSchemas[schemaName].relationships?.[include[0]] 20 | } 21 | 22 | if (!allSchemas[schemaName].relationships?.[include[0]]) { 23 | return false 24 | } 25 | 26 | return isValidInclude( 27 | allSchemas[schemaName].relationships?.[include[0]].targetSchema as string, 28 | include.slice(1), 29 | allSchemas, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/node/src/parse/body.ts: -------------------------------------------------------------------------------- 1 | import coBody from "co-body" 2 | 3 | export const parseHatchifyBody = coBody 4 | -------------------------------------------------------------------------------- /packages/node/src/parse/getColumnName.spec.ts: -------------------------------------------------------------------------------- 1 | import { getColumnName } from "./getColumnName.js" 2 | 3 | describe("getColumnName", () => { 4 | it("escapes the column name without dialect", () => { 5 | expect(getColumnName("word")).toBe("word") 6 | expect(getColumnName("twoWords")).toBe("two_words") 7 | expect(getColumnName("itemA.itemB")).toBe("itemA.item_b") 8 | }) 9 | 10 | it("escapes the column name for SQLite", () => { 11 | expect(getColumnName("word", "sqlite")).toBe("word") 12 | expect(getColumnName("twoWords", "sqlite")).toBe("two_words") 13 | expect(getColumnName("itemA.itemB", "sqlite")).toBe("`itemA.item_b`") 14 | }) 15 | 16 | it("escapes the column name for Postgres", () => { 17 | expect(getColumnName("word", "postgres")).toBe("word") 18 | expect(getColumnName("twoWords", "postgres")).toBe("two_words") 19 | expect(getColumnName("itemA.itemB", "postgres")).toBe(`"itemA.item_b"`) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/node/src/parse/getColumnName.ts: -------------------------------------------------------------------------------- 1 | import { snakeCase } from "lodash-es" 2 | import type { Dialect } from "sequelize" 3 | 4 | export function getColumnName( 5 | attributeName: string, 6 | escapeStyle?: Dialect, 7 | ): string { 8 | if (!attributeName.includes(".")) { 9 | return snakeCase(attributeName) 10 | } 11 | 12 | const columnName = attributeName 13 | .replaceAll("$", "") 14 | .split(".") 15 | .map((part, index, { length }) => 16 | index === length - 1 ? snakeCase(part) : part, 17 | ) 18 | .join(".") 19 | 20 | if (escapeStyle === "sqlite") { 21 | return `\`${columnName}\`` 22 | } 23 | if (escapeStyle === "postgres") { 24 | return `"${columnName}"` 25 | } 26 | return columnName 27 | } 28 | -------------------------------------------------------------------------------- /packages/node/src/parse/isPathIncluded.ts: -------------------------------------------------------------------------------- 1 | export function isPathIncluded( 2 | flatIncludes: string[], 3 | relationshipPath: string[], 4 | ): boolean { 5 | if (relationshipPath.length < 2) { 6 | return true 7 | } 8 | 9 | if ( 10 | !flatIncludes.includes( 11 | relationshipPath.slice(0, relationshipPath.length - 1).join("."), 12 | ) 13 | ) { 14 | return false 15 | } 16 | 17 | if (relationshipPath.length === 2) { 18 | return true 19 | } 20 | 21 | return isPathIncluded( 22 | flatIncludes, 23 | relationshipPath.slice(0, relationshipPath.length - 1), 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /packages/node/src/parse/isValidAttribute.ts: -------------------------------------------------------------------------------- 1 | import type { FinalSchema } from "@hatchifyjs/core" 2 | 3 | import type { ModelFunctionsCollection } from "../types.js" 4 | 5 | export function isValidAttribute( 6 | schemaName: string, 7 | relationshipPath: string[], 8 | allSchemas: ModelFunctionsCollection, 9 | ): boolean { 10 | if (!allSchemas[schemaName]) { 11 | return false 12 | } 13 | 14 | if (relationshipPath.length === 0) { 15 | return false 16 | } 17 | 18 | if (relationshipPath.length === 1) { 19 | return ( 20 | relationshipPath[0] === "id" || 21 | !!allSchemas[schemaName].attributes[relationshipPath[0]] 22 | ) 23 | } 24 | 25 | if (!allSchemas[schemaName].relationships?.[relationshipPath[0]]) { 26 | return false 27 | } 28 | 29 | return isValidAttribute( 30 | allSchemas[schemaName].relationships?.[relationshipPath[0]] 31 | .targetSchema as string, 32 | relationshipPath.slice(1), 33 | allSchemas, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /packages/node/src/parse/walk.ts: -------------------------------------------------------------------------------- 1 | export const walk: ( 2 | maybeObj: T | any[], 3 | fn: (key: string | symbol, value: unknown) => void, 4 | ) => T | any[] = (maybeObj, fn) => { 5 | if (Array.isArray(maybeObj)) { 6 | return maybeObj.map((item) => walk(item, fn)) 7 | } else if (maybeObj && typeof maybeObj === "object") { 8 | const keys = [ 9 | ...Object.getOwnPropertyNames(maybeObj), 10 | ...Object.getOwnPropertySymbols(maybeObj), 11 | ] 12 | const entries = keys.map((k) => [ 13 | k, 14 | (maybeObj as { [key: typeof k]: any })[k], 15 | ]) 16 | return entries.reduce((acc, [key, value]) => { 17 | const [newVal, newKey = key] = 18 | (fn(key, value) as unknown as [unknown, string | symbol]) ?? [] 19 | return { 20 | ...acc, 21 | [newKey]: newVal ?? walk(value, fn), 22 | } 23 | }, {}) as typeof maybeObj 24 | } else { 25 | return maybeObj 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/node/src/schema.ts: -------------------------------------------------------------------------------- 1 | import type { FinalSchema } from "@hatchifyjs/core" 2 | 3 | import type { Hatchify } from "./node.js" 4 | import { HatchifySymbolModel } from "./types.js" 5 | 6 | export function buildSchemaForModel( 7 | hatchify: Hatchify, 8 | modelName: string, 9 | ): FinalSchema { 10 | return hatchify.model[modelName]?.[HatchifySymbolModel] 11 | } 12 | -------------------------------------------------------------------------------- /packages/node/src/sequelize/customTypes/CustomDecimal.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyCoerceError } from "@hatchifyjs/core" 2 | import { DataTypes } from "sequelize" 3 | 4 | // @ts-expect-error 5 | export class CustomDecimal extends DataTypes.DECIMAL { 6 | static parse(value: string): number { 7 | const parsed = parseFloat(value) 8 | 9 | if ([Infinity, -Infinity].includes(parsed)) { 10 | throw new HatchifyCoerceError( 11 | "Retrieved number is outside of the JavaScript number range", 12 | ) 13 | } 14 | 15 | return parsed 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/node/src/sequelize/customTypes/CustomInteger.ts: -------------------------------------------------------------------------------- 1 | import { HatchifyCoerceError } from "@hatchifyjs/core" 2 | import { DataTypes } from "sequelize" 3 | 4 | // @ts-expect-error 5 | export class CustomInteger extends DataTypes.INTEGER { 6 | static parse(value: string): number { 7 | const parsed = parseInt(value) 8 | 9 | if (parsed < Number.MIN_SAFE_INTEGER || parsed > Number.MAX_SAFE_INTEGER) { 10 | throw new HatchifyCoerceError( 11 | "Retrieved number is outside of the JavaScript safe integer range", 12 | ) 13 | } 14 | 15 | return parsed 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/node/src/sequelize/getSequelizeSchemaName.spec.ts: -------------------------------------------------------------------------------- 1 | import { getSequelizeSchemaName } from "./getSequelizeSchemaName.js" 2 | 3 | describe("getSequelizeSchemaName", () => { 4 | it("is always undefined for sqlite", () => { 5 | expect(getSequelizeSchemaName("sqlite", "schemaName")).toBeUndefined() 6 | expect(getSequelizeSchemaName("sqlite", "")).toBeUndefined() 7 | expect(getSequelizeSchemaName("sqlite", undefined)).toBeUndefined() 8 | expect( 9 | getSequelizeSchemaName("sqlite", null as unknown as string), 10 | ).toBeUndefined() 11 | }) 12 | 13 | it("is always defined for postgres", () => { 14 | expect(getSequelizeSchemaName("postgres", "schemaName")).toBe("schema_name") 15 | expect(getSequelizeSchemaName("postgres", "")).toBe("public") 16 | expect(getSequelizeSchemaName("postgres", undefined)).toBe("public") 17 | expect(getSequelizeSchemaName("postgres", null as unknown as string)).toBe( 18 | "public", 19 | ) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/node/src/sequelize/getSequelizeSchemaName.ts: -------------------------------------------------------------------------------- 1 | import { snakeCase } from "lodash-es" 2 | import type { Dialect } from "sequelize" 3 | 4 | export function getSequelizeSchemaName( 5 | dialect: Dialect, 6 | namespace?: string, 7 | ): string | undefined { 8 | if (dialect !== "postgres") { 9 | return undefined 10 | } 11 | 12 | if (!namespace) { 13 | return "public" 14 | } 15 | 16 | return snakeCase(namespace) 17 | } 18 | -------------------------------------------------------------------------------- /packages/node/src/sequelize/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./convertHatchifyModels.js" 2 | export * from "./createSequelizeInstance.js" 3 | -------------------------------------------------------------------------------- /packages/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "rootDir": "src", 7 | "outDir": "dist", 8 | "sourceMap": true, 9 | "strict": true, 10 | "strictNullChecks": true 11 | }, 12 | "exclude": ["dist/*"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-jsonapi/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@types/react", "@vitest/coverage-c8", "@hatchifyjs/rest-client"] 2 | -------------------------------------------------------------------------------- /packages/react-jsonapi/README.md: -------------------------------------------------------------------------------- 1 | # @hatchifyjs/react-jsonapi 2 | 3 | `@hatchifyjs/react-jsonapi` is part of the [Hatchify](https://github.com/bitovi/hatchify) toolkit and provides data models for React. 4 | 5 | For more information, see [`@hatchifyjs/react-jsonapi`'s documentation page](https://github.com/bitovi/hatchify/blob/main/docs/react-jsonapi/README.md) 6 | -------------------------------------------------------------------------------- /packages/react-jsonapi/src/react-jsonapi.tsx: -------------------------------------------------------------------------------- 1 | export type { 2 | CreateType, 3 | FlatCreateType, 4 | FlatUpdateType, 5 | RecordType, 6 | UpdateType, 7 | } from "@hatchifyjs/rest-client" 8 | export { default as createJsonapiClient } from "@hatchifyjs/rest-client-jsonapi" 9 | export { default as hatchifyReactRest } from "@hatchifyjs/react-rest" 10 | -------------------------------------------------------------------------------- /packages/react-jsonapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "extends": "../../tsconfig.json", 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-rest/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@vitest/coverage-c8", "@types/react"] 2 | -------------------------------------------------------------------------------- /packages/react-rest/doc/attachments/relationships.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/react-rest/doc/attachments/relationships.png -------------------------------------------------------------------------------- /packages/react-rest/doc/attachments/todo-app-with-relationships.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/react-rest/doc/attachments/todo-app-with-relationships.png -------------------------------------------------------------------------------- /packages/react-rest/doc/attachments/todo-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/react-rest/doc/attachments/todo-app.png -------------------------------------------------------------------------------- /packages/react-rest/doc/attachments/ts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/react-rest/doc/attachments/ts.gif -------------------------------------------------------------------------------- /packages/react-rest/doc/attachments/ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/react-rest/doc/attachments/ui.gif -------------------------------------------------------------------------------- /packages/react-rest/src/react-rest.ts: -------------------------------------------------------------------------------- 1 | import { hatchifyReactRest } from "./services/index.js" 2 | 3 | export type { HatchifyReactRest } from "./services/index.js" 4 | 5 | export { 6 | useCreateOne, 7 | useDeleteOne, 8 | useAll, 9 | useOne, 10 | useUpdateOne, 11 | } from "./services/index.js" 12 | 13 | export default hatchifyReactRest 14 | -------------------------------------------------------------------------------- /packages/react-rest/src/services/hatchifyReactRest/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hatchifyReactRest.js" 2 | -------------------------------------------------------------------------------- /packages/react-rest/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hatchifyReactRest/index.js" 2 | export * from "./useCreateOne/index.js" 3 | export * from "./useDeleteOne/index.js" 4 | export * from "./useAll/index.js" 5 | export * from "./useOne/index.js" 6 | export * from "./useUpdateOne/index.js" 7 | -------------------------------------------------------------------------------- /packages/react-rest/src/services/useAll/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useAll.js" 2 | -------------------------------------------------------------------------------- /packages/react-rest/src/services/useCreateOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useCreateOne.js" 2 | -------------------------------------------------------------------------------- /packages/react-rest/src/services/useDeleteOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useDeleteOne.js" 2 | -------------------------------------------------------------------------------- /packages/react-rest/src/services/useOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useOne.js" 2 | -------------------------------------------------------------------------------- /packages/react-rest/src/services/useUpdateOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useUpdateOne.js" 2 | -------------------------------------------------------------------------------- /packages/react-rest/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react-rest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-rest/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import dts from "vite-plugin-dts" 3 | import nodeExternals from "rollup-plugin-node-externals" 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | build: { 8 | lib: { 9 | entry: "src/react-rest.ts", 10 | formats: ["es", "cjs"], 11 | }, 12 | }, 13 | plugins: [{ ...nodeExternals(), enforce: "pre" }, dts()], 14 | }) 15 | -------------------------------------------------------------------------------- /packages/react-ui/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@vitest/coverage-c8", "@hatchifyjs/core", "@testing-library/jest-dom"] 2 | -------------------------------------------------------------------------------- /packages/react-ui/README.md: -------------------------------------------------------------------------------- 1 | # @hatchifyjs/react-ui 2 | 3 | React components that tie together design-\* components with React Rest data-fetching. 4 | 5 | This isn't used directly. Use [`@hatchifyjs/react`](https://github.com/bitovi/hatchify/blob/main/docs/react/README.md) instead. 6 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyCollection/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/react-ui/src/components/HatchifyCollection/index.ts -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyColumn/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HatchifyColumn.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyDataGrid/index.ts: -------------------------------------------------------------------------------- 1 | export type { HatchifyDataGridProps } from "./HatchifyDataGrid.js" 2 | export { 3 | default as HatchifyDataGrid, 4 | getDefaultInclude, 5 | } from "./HatchifyDataGrid.js" 6 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyEmpty/HatchifyEmpty.tsx: -------------------------------------------------------------------------------- 1 | export type HatchifyEmptyProps = { 2 | children: React.ReactNode 3 | } 4 | 5 | export const HatchifyEmpty: React.FC = () => { 6 | return null 7 | } 8 | 9 | HatchifyEmpty.displayName = "Empty" 10 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyEmpty/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HatchifyEmpty.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyEverything/components/NoSchemas.tsx: -------------------------------------------------------------------------------- 1 | import { useHatchifyPresentation } from "../../HatchifyPresentationProvider/index.js" 2 | 3 | export function NoSchemas(): JSX.Element { 4 | const { Everything } = useHatchifyPresentation() 5 | 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyEverything/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./NoSchemas.js" 2 | export * from "./WithSchemas.js" 3 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyEverything/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HatchifyEverything } from "./HatchifyEverything.js" 2 | export type { HatchifyEverythingProps } from "./HatchifyEverything.js" 3 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyFilters/HatchifyFilters.tsx: -------------------------------------------------------------------------------- 1 | import type { PartialSchema } from "@hatchifyjs/core" 2 | import type { GetSchemaNames } from "@hatchifyjs/rest-client" 3 | import type { XDataGridProps } from "../../react-ui.js" 4 | import { useHatchifyPresentation } from "../index.js" 5 | 6 | function HatchifyFilters< 7 | const TSchemas extends Record, 8 | const TSchemaName extends GetSchemaNames, 9 | >(props: XDataGridProps): JSX.Element { 10 | const { Filters } = useHatchifyPresentation() 11 | return 12 | } 13 | 14 | export default HatchifyFilters 15 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyFilters/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HatchifyFilters } from "./HatchifyFilters.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyList/HatchifyList.tsx: -------------------------------------------------------------------------------- 1 | import type { PartialSchema } from "@hatchifyjs/core" 2 | import type { GetSchemaNames } from "@hatchifyjs/rest-client" 3 | import type { XDataGridProps } from "../../react-ui.js" 4 | import { useHatchifyPresentation } from "../index.js" 5 | 6 | function HatchifyList< 7 | const TSchemas extends Record, 8 | const TSchemaName extends GetSchemaNames, 9 | >(props: XDataGridProps): JSX.Element { 10 | const { List } = useHatchifyPresentation() 11 | return 12 | } 13 | 14 | export default HatchifyList 15 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyList/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HatchifyList } from "./HatchifyList.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyNavigation/HatchifyNavigation.tsx: -------------------------------------------------------------------------------- 1 | import type { FinalSchemas } from "@hatchifyjs/rest-client" 2 | import type { PartialSchema } from "@hatchifyjs/core" 3 | import { useHatchifyPresentation } from "../index.js" 4 | 5 | export interface HatchifyNavigationProps< 6 | TSchemas extends Record, 7 | > { 8 | finalSchemas: FinalSchemas 9 | partialSchemas: TSchemas 10 | activeTab: string | undefined 11 | onTabChange: (schema: string) => void 12 | children?: React.ReactNode | null 13 | } 14 | 15 | function HatchifyNavigation< 16 | const TSchemas extends Record, 17 | >({ ...props }: HatchifyNavigationProps): JSX.Element { 18 | const { Navigation } = useHatchifyPresentation() 19 | return 20 | } 21 | 22 | HatchifyNavigation.displayName = "Navigation" 23 | 24 | export default HatchifyNavigation 25 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyNavigation/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HatchifyNavigation } from "./HatchifyNavigation.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyNoSchemas/HatchifyNoSchemas.tsx: -------------------------------------------------------------------------------- 1 | import { useHatchifyPresentation } from "../index.js" 2 | 3 | function HatchifyNoSchemas(): JSX.Element { 4 | const { NoSchemas } = useHatchifyPresentation() 5 | return 6 | } 7 | 8 | HatchifyNoSchemas.displayName = "NoSchemas" 9 | 10 | export default HatchifyNoSchemas 11 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyNoSchemas/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HatchifyNoSchemas } from "./HatchifyNoSchemas.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyPagination/HatchifyPagination.tsx: -------------------------------------------------------------------------------- 1 | import type { PartialSchema } from "@hatchifyjs/core" 2 | import type { GetSchemaNames } from "@hatchifyjs/rest-client" 3 | import type { XDataGridProps } from "../../react-ui.js" 4 | import { useHatchifyPresentation } from "../index.js" 5 | 6 | function HatchifyPagination< 7 | const TSchemas extends Record, 8 | const TSchemaName extends GetSchemaNames, 9 | >(props: XDataGridProps): JSX.Element { 10 | const { Pagination } = useHatchifyPresentation() 11 | return 12 | } 13 | 14 | export default HatchifyPagination 15 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyPagination/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HatchifyPagination } from "./HatchifyPagination.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyPresentationProvider/DefaultDisplayComponents/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | String, 3 | StringList, 4 | Number, 5 | NumberList, 6 | Boolean, 7 | BooleanList, 8 | Date, 9 | DateList, 10 | Relationship, 11 | RelationshipList, 12 | } from "./DefaultDisplayComponents.js" 13 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyPresentationProvider/DefaultFieldComponents/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | String, 3 | Number, 4 | Boolean, 5 | Date, 6 | Relationship, 7 | } from "./DefaultFieldComponents.js" 8 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/HatchifyPresentationProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useHatchifyPresentation, 3 | HatchifyPresentationDefaultDisplayComponents, 4 | HatchifyPresentationDefaultFieldComponents, 5 | HatchifyPresentationProvider, 6 | } from "./HatchifyPresentationProvider.js" 7 | export type { 8 | DefaultDisplayComponentsTypes, 9 | DefaultFieldComponentsTypes, 10 | } from "./HatchifyPresentationProvider.js" 11 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HatchifyDataGrid/index.js" 2 | export * from "./HatchifyPresentationProvider/index.js" 3 | export * from "./HatchifyEmpty/index.js" 4 | -------------------------------------------------------------------------------- /packages/react-ui/src/hatchifyReact/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hatchifyReact.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useDataGridState } from "./useDataGridState.js" 2 | export { default as useCompoundComponents } from "./useCompoundComponents/useCompoundComponents.js" 3 | export { default as usePage } from "./usePage.js" 4 | export { default as useSelected } from "./useSelected.js" 5 | export { default as useSort } from "./useSort.js" 6 | export type { HatchifyColumn } from "./useCompoundComponents/useCompoundComponents.js" 7 | export type { DataGridState } from "./useDataGridState.js" 8 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useCompoundComponents/helpers/getEmptyList.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest" 2 | import { getEmptyList } from "./index.js" 3 | 4 | describe("hooks/useCompoundComponents/helpers/getEmptyList", () => { 5 | it.todo("works when valid children are passed", () => { 6 | // When using `HatchifyEmpty` in this test, it is filtered out because in 7 | // the test environment its name property is resolving to `HatchifyEmpty` 8 | // and not `Empty`. This is not an issue when running outside of a test 9 | // environment. 10 | }) 11 | 12 | it("falls back to default EmptyList", () => { 13 | const EmptyList = getEmptyList([]) 14 | 15 | expect(EmptyList).toBeInstanceOf(Function) 16 | expect(EmptyList()).toMatchInlineSnapshot(` 17 |
18 | No records found 19 |
20 | `) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useCompoundComponents/helpers/getEmptyList.tsx: -------------------------------------------------------------------------------- 1 | export function getEmptyList(childArray: JSX.Element[]): () => JSX.Element { 2 | const emptyComponent = childArray.find( 3 | (c) => c.type.name === "Empty" || c.type.displayName === "Empty", 4 | ) 5 | const emptyDisplay: JSX.Element = emptyComponent?.props.children || undefined 6 | 7 | const EmptyList = () => emptyDisplay ||
No records found
8 | 9 | return EmptyList 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useCompoundComponents/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getColumn.js" 2 | export * from "./getColumns.js" 3 | export * from "./getColumnsFromSchema.js" 4 | export * from "./getDefaultDataRender.js" 5 | export * from "./getEmptyList.js" 6 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useFilter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { renderHook, waitFor } from "@testing-library/react" 3 | import useFilter from "./useFilter.js" 4 | 5 | describe("usePage", () => { 6 | it("works", async () => { 7 | const { result } = renderHook(() => useFilter()) 8 | 9 | expect(result.current.filter).toEqual(undefined) 10 | 11 | await waitFor(() => { 12 | result.current.setFilter([ 13 | { field: "name", value: "Walk the dog", operator: "$eq" }, 14 | ]) 15 | }) 16 | 17 | expect(result.current.filter).toEqual([ 18 | { field: "name", value: "Walk the dog", operator: "$eq" }, 19 | ]) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useFilter.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import type { Filters } from "@hatchifyjs/rest-client" 3 | import type { HatchifyDataGridFilters } from "../presentation/index.js" 4 | 5 | export default function useFilter(): HatchifyDataGridFilters { 6 | const [filter, setFilter] = useState(undefined) 7 | 8 | return { 9 | filter, 10 | setFilter, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/usePage.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { renderHook, waitFor } from "@testing-library/react" 3 | import usePage from "./usePage.js" 4 | 5 | describe("usePage", () => { 6 | it("works", async () => { 7 | const { result } = renderHook(() => usePage()) 8 | 9 | expect(result.current.page).toEqual({ 10 | size: 10, 11 | number: 1, 12 | }) 13 | 14 | await waitFor(() => { 15 | result.current.setPage({ size: 10, number: 2 }) 16 | }) 17 | 18 | expect(result.current.page).toEqual({ 19 | size: 10, 20 | number: 2, 21 | }) 22 | 23 | await waitFor(() => { 24 | result.current.setPage({ size: 15, number: 12 }) 25 | }) 26 | 27 | expect(result.current.page).toEqual({ 28 | size: 15, 29 | number: 12, 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/usePage.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import type { 3 | HatchifyDataGridPage, 4 | PageCountObject, 5 | } from "../presentation/index.js" 6 | import type { PaginationObject } from "@hatchifyjs/rest-client" 7 | 8 | // todo (future): customizable pagination.size? 9 | // todo (future): page number can come from query string 10 | export default function usePage( 11 | defaultPage?: PaginationObject, 12 | ): HatchifyDataGridPage { 13 | const [page, setPage] = useState( 14 | defaultPage ?? { 15 | number: 1, 16 | size: 10, 17 | }, 18 | ) 19 | 20 | return { 21 | page, 22 | setPage, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useSelected.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import type { 3 | HatchifyDataGridSelected, 4 | HatchifyDataGridSelectedState, 5 | } from "../presentation/index.js" 6 | 7 | export default function useSelected( 8 | defaultSelected?: HatchifyDataGridSelectedState, 9 | onSelectedChange?: (selected: HatchifyDataGridSelectedState) => void, 10 | ): HatchifyDataGridSelected { 11 | const [selected, setSelected] = useState( 12 | defaultSelected || { all: false, ids: [] }, 13 | ) 14 | 15 | const setSelectedWrapper: ( 16 | selected: HatchifyDataGridSelectedState, 17 | ) => void = (selected) => { 18 | setSelected(selected) 19 | if (onSelectedChange) { 20 | onSelectedChange(selected) 21 | } 22 | } 23 | 24 | return { 25 | selected, 26 | setSelected: setSelectedWrapper, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/react-ui/src/presentation/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./interfaces.js" 2 | -------------------------------------------------------------------------------- /packages/react-ui/src/react-ui.ts: -------------------------------------------------------------------------------- 1 | import { hatchifyReact } from "./hatchifyReact/index.js" 2 | 3 | export type { 4 | CreateType, 5 | FlatCreateType, 6 | FlatUpdateType, 7 | RecordType, 8 | UpdateType, 9 | } from "@hatchifyjs/rest-client" 10 | 11 | export type { HatchifyApp } from "./hatchifyReact/index.js" 12 | 13 | export * from "./components/index.js" 14 | export * from "./presentation/index.js" 15 | export * from "./hooks/index.js" 16 | 17 | export default hatchifyReact 18 | -------------------------------------------------------------------------------- /packages/react-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | }, 5 | "extends": "../../tsconfig.json", 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite" 3 | import dts from "vite-plugin-dts" 4 | import react from "@vitejs/plugin-react" 5 | import nodeExternals from "rollup-plugin-node-externals" 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | build: { 10 | lib: { 11 | entry: "src/react-ui.ts", 12 | formats: ["es"], 13 | }, 14 | }, 15 | plugins: [{ ...nodeExternals(), enforce: "pre" }, dts(), react()], 16 | test: { 17 | globals: true, 18 | environment: "jsdom", 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/react/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@types/react", "@vitest/coverage-c8"] 2 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # @hatchifyjs/react 2 | 3 | `@hatchifyjs/react` is part of the [Hatchify](https://github.com/bitovi/hatchify) toolkit and provides data models and low-code like Components for React. 4 | 5 | For more information, see [`@hatchifyjs/react`'s documentation page](https://github.com/bitovi/hatchify/blob/main/docs/react/README.md) 6 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hatchifyjs/react", 3 | "version": "0.2.3", 4 | "type": "module", 5 | "exports": "./dist/react.js", 6 | "types": "./dist/react.d.ts", 7 | "scripts": { 8 | "prepublish": "npm run clean && npm run build", 9 | "typecheck": "tsc --noEmit", 10 | "lint": "eslint src", 11 | "depcheck": "depcheck .", 12 | "test": "vitest run", 13 | "test:dev": "vitest", 14 | "test:coverage": "vitest run --coverage", 15 | "clean": "rm -rf tsconfig.tsbuildinfo dist", 16 | "build": "tsc" 17 | }, 18 | "dependencies": { 19 | "@hatchifyjs/design-mui": "^0.2.2", 20 | "@hatchifyjs/react-ui": "^0.2.2", 21 | "@hatchifyjs/rest-client-jsonapi": "^0.1.41" 22 | }, 23 | "devDependencies": { 24 | "@types/react": "^18.2.0", 25 | "jsdom": "^21.1.1", 26 | "vitest": "^0.30.1" 27 | }, 28 | "peerDependencies": { 29 | "@hatchifyjs/core": "^0.4.x", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/react/src/react.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | 3 | describe("react", () => it("todo", () => expect(true).toBe(true))) 4 | -------------------------------------------------------------------------------- /packages/react/src/react.tsx: -------------------------------------------------------------------------------- 1 | export type { PartialSchema } from "@hatchifyjs/core" 2 | export { default as hatchifyReact } from "@hatchifyjs/react-ui" 3 | export type { 4 | DataGridState, 5 | FlatCreateType as CreateType, 6 | FlatUpdateType as UpdateType, 7 | HatchifyApp, 8 | RecordType, 9 | } from "@hatchifyjs/react-ui" 10 | export { 11 | default as HatchifyProvider, 12 | List, 13 | Pagination, 14 | Filters, 15 | DataGrid, 16 | } from "@hatchifyjs/design-mui" 17 | export { 18 | convertToHatchifyResources, 19 | default as createJsonapiClient, 20 | } from "@hatchifyjs/rest-client-jsonapi" 21 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "extends": "../../tsconfig.json", 8 | "include": ["src"] 9 | } -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@vitest/coverage-c8"] 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/README.md: -------------------------------------------------------------------------------- 1 | # @hatchifyjs/rest-client-jsonapi 2 | 3 | Responsible for making network requests to a JSON:API-compliant backend. 4 | 5 | This isn't used directly. Use [`@hatchifyjs/react-jsonapi`](https://github.com/bitovi/hatchify/blob/main/docs/react-jsonapi/README.md) instead. 6 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from "msw/node" 2 | import { handlers } from "./handlers.js" 3 | 4 | export const server = setupServer(...handlers) 5 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/rest-client-jsonapi.ts: -------------------------------------------------------------------------------- 1 | import { jsonapi } from "./services/index.js" 2 | 3 | export { 4 | convertToHatchifyResources, 5 | createOne, 6 | findAll, 7 | findOne, 8 | updateOne, 9 | } from "./services/index.js" 10 | export default jsonapi 11 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/createOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/deleteOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deleteOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/findAll/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./findAll.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/findOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./findOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createOne/index.js" 2 | export * from "./deleteOne/index.js" 3 | export * from "./findAll/index.js" 4 | export * from "./findOne/index.js" 5 | export * from "./jsonapi/index.js" 6 | export * from "./updateOne/index.js" 7 | export * from "./utils/index.js" 8 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/jsonapi/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./jsonapi.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/updateOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./updateOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/utils/fetch/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fetch.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fetch/index.js" 2 | export * from "./resources/index.js" 3 | export * from "./query/index.js" 4 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/utils/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./query.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/services/utils/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./resources.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/rest-client-jsonapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "rootDir": "src", 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/rest-client/.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ["@vitest/coverage-c8"] 2 | -------------------------------------------------------------------------------- /packages/rest-client/README.md: -------------------------------------------------------------------------------- 1 | # @hatchifyjs/rest-client 2 | 3 | Data-fetching functions that are not framework-specific. 4 | 5 | This isn't used directly. Use [`@hatchifyjs/react-jsonapi`](https://github.com/bitovi/hatchify/blob/main/docs/react-jsonapi/README.md) instead. 6 | -------------------------------------------------------------------------------- /packages/rest-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hatchifyjs/rest-client", 3 | "version": "0.1.29", 4 | "type": "module", 5 | "exports": "./dist/rest-client.js", 6 | "types": "./dist/rest-client.d.ts", 7 | "scripts": { 8 | "prepublish": "npm run clean && npm run build", 9 | "typecheck": "tsc --noEmit", 10 | "lint": "eslint src", 11 | "depcheck": "depcheck .", 12 | "test": "vitest run", 13 | "test:dev": "vitest", 14 | "test:coverage": "vitest run --coverage", 15 | "clean": "rm -rf tsconfig.tsbuildinfo dist", 16 | "build": "tsc" 17 | }, 18 | "devDependencies": { 19 | "@types/lodash.isempty": "^4.4.9", 20 | "@vitest/coverage-c8": "^0.32.2", 21 | "vitest": "^0.30.1" 22 | }, 23 | "peerDependencies": { 24 | "@hatchifyjs/core": "^0.4.x" 25 | }, 26 | "dependencies": { 27 | "lodash.isempty": "^4.4.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./promise/index.js" 2 | export * from "./store/index.js" 3 | export * from "./subscribe/index.js" 4 | export * from "./types/index.js" 5 | export * from "./utils/index.js" 6 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/promise/createOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/promise/deleteOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./deleteOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/promise/findAll/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./findAll.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/promise/findOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./findOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/promise/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createOne/index.js" 2 | export * from "./deleteOne/index.js" 3 | export * from "./findAll/index.js" 4 | export * from "./findOne/index.js" 5 | export * from "./updateOne/index.js" 6 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/promise/updateOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./updateOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./store.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/subscribe/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./subscribeToAll/index.js" 2 | export * from "./subscribeToOne/index.js" 3 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/subscribe/subscribeToAll/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./subscribeToAll.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/subscribe/subscribeToAll/subscribeToAll.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest" 2 | import { createStore } from "../../store/index.js" 3 | import { subscribeToAll } from "./subscribeToAll.js" 4 | 5 | describe("rest-client/services/subscribe/subscribeToAll", () => { 6 | it("callback should be called from store subscribers", () => { 7 | const store = createStore(["articles"]) 8 | const spy = vi.fn() 9 | 10 | subscribeToAll("articles", undefined, spy) 11 | store.articles.subscribers.forEach((fn) => fn([])) 12 | 13 | expect(spy).toHaveBeenCalledTimes(1) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/subscribe/subscribeToOne/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./subscribeToOne.js" 2 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/subscribe/subscribeToOne/subscribeToOne.ts: -------------------------------------------------------------------------------- 1 | import { getStore } from "../../store/index.js" 2 | import type { Record, Unsubscribe } from "../../types/index.js" 3 | 4 | /** 5 | * Adds a subscriber to the store for a given schema and id. Will only notify 6 | * the subscriber when the record with the given id is updated. 7 | */ 8 | export const subscribeToOne = ( 9 | resource: string, 10 | id: string, 11 | onChange: (data: Record) => void, 12 | ): Unsubscribe => { 13 | const store = getStore(resource) 14 | 15 | function notify(data: Record[]) { 16 | const record = data.find((record) => record.id === id) 17 | if (record) { 18 | onChange(record) 19 | } 20 | } 21 | 22 | store.subscribers.push(notify) 23 | 24 | return () => { 25 | store.subscribers = store.subscribers.filter((fn) => fn !== notify) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./data.js" 2 | export * from "./meta.js" 3 | export * from "./query.js" 4 | export * from "./schema.js" 5 | export * from "./rest-client.js" 6 | export * from "./subscribe.js" 7 | export * from "./utility.js" 8 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/types/subscribe.ts: -------------------------------------------------------------------------------- 1 | import type { PartialSchema } from "@hatchifyjs/core" 2 | import type { GetSchemaFromName, GetSchemaNames, RecordType } from "./index.js" 3 | 4 | export type Unsubscribe = () => void 5 | 6 | export type Subscription = < 7 | const TSchemas extends Record, 8 | const TSchemaName extends GetSchemaNames, 9 | >( 10 | data: Array>>, 11 | ) => void 12 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/types/utility.ts: -------------------------------------------------------------------------------- 1 | export type WithRequiredProperty = Type & { 2 | [Property in Key]-?: Type[Property] 3 | } 4 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./meta.js" 2 | export * from "./records.js" 3 | export * from "./request.js" 4 | export * from "./response.js" 5 | export * from "./schema.js" 6 | export * from "./selector.js" 7 | export * from "./unflatten.js" 8 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/utils/notify.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitovi/hatchify/a47ddb72bf2c0d93931eafa90d996546adf20f83/packages/rest-client/src/services/utils/notify.ts -------------------------------------------------------------------------------- /packages/rest-client/src/services/utils/schema.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest" 2 | import { schemaNameIsString } from "./schema.js" 3 | 4 | describe("rest-client/services/utils/schema", () => { 5 | describe("scehmaNameIsString", () => { 6 | it("works", () => { 7 | expect(schemaNameIsString("Foo")).toBe(true) 8 | expect(schemaNameIsString("Bar")).toBe(true) 9 | expect(schemaNameIsString(5)).toBe(false) 10 | expect(schemaNameIsString(true)).toBe(false) 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /packages/rest-client/src/services/utils/schema.ts: -------------------------------------------------------------------------------- 1 | export function schemaNameIsString(schemaName: unknown): schemaName is string { 2 | return typeof schemaName === "string" 3 | } 4 | 5 | export class SchemaNameNotStringError extends Error { 6 | constructor(schemaName: unknown) { 7 | super(`Expected schemaName to be a string, received ${typeof schemaName}`) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/rest-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "rootDir": "src", 7 | "outDir": "dist" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/check-packages.ts: -------------------------------------------------------------------------------- 1 | import semver from "semver" 2 | 3 | import packages from "./lib/packages" 4 | 5 | for (const pkg of Object.values(packages)) { 6 | if (!semver.valid(pkg.version)) { 7 | console.error(`${pkg.name} - Invalid version: ${pkg.version}`) 8 | } 9 | 10 | if ("private" in pkg) { 11 | console.error(`${pkg.name} - Package should not be private.`) 12 | } 13 | 14 | if (pkg.dependencies) { 15 | for (const [name, range] of Object.entries(pkg.dependencies)) { 16 | if (packages[name]) { 17 | if (!semver.satisfies(packages[name].version, range)) { 18 | console.error(`${pkg.name} - Unsatisfied package: ${name}@${range}`) 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/lib/packages.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | 4 | interface Package { 5 | name: string 6 | private?: boolean 7 | version: string 8 | dependencies?: Record 9 | devDependencies?: Record 10 | } 11 | 12 | const packagesPath = path.join(__dirname, "..", "..", "packages") 13 | 14 | const list = fs 15 | .readdirSync(packagesPath) 16 | .map((name) => path.join(packagesPath, name, "package.json")) 17 | .filter((pkg) => fs.existsSync(pkg)) 18 | .map((pkg) => JSON.parse(fs.readFileSync(pkg, "utf-8")) as Package) 19 | 20 | const packages: Record = {} 21 | export default packages 22 | 23 | for (const pkg of list) { 24 | packages[pkg.name] = pkg 25 | } 26 | -------------------------------------------------------------------------------- /scripts/pree2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd e2e; 3 | rm -rf hatchify-app; 4 | node ../packages/create/index.js hatchify-app \ 5 | --frontend ${npm_config_frontend} \ 6 | --backend ${npm_config_backend} \ 7 | --database ${npm_config_database} \ 8 | --path ../../packages; 9 | cp schemas.txt hatchify-app/schemas.ts; 10 | cp App.txt hatchify-app/frontend/App.tsx; 11 | cp main.txt hatchify-app/main.tsx; 12 | -------------------------------------------------------------------------------- /scripts/updategs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd example; 3 | rm -rf getting-started; 4 | node ../packages/create/index.js getting-started \ 5 | --frontend=react \ 6 | --backend=koa \ 7 | --database=sqlite://localhost/:memory 8 | --------------------------------------------------------------------------------