├── .envrc ├── .github ├── actions │ ├── install │ │ └── action.yml │ ├── publish │ │ └── action.yml │ ├── setup-nix │ │ └── action.yml │ └── setup │ │ └── action.yml ├── docker │ ├── Dockerfile.mysql │ └── Dockerfile.postgres ├── renovate.json └── workflows │ ├── create-draft-release.yml │ ├── release-alpha.yml │ ├── release-latest.yml │ ├── tag-release.yml │ └── tests.yml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── RELEASE_WORKFLOW.md ├── docs ├── .gitignore ├── .npmrc ├── .vscode │ ├── extensions.json │ ├── settings.json │ └── tasks.json ├── README.md ├── components │ ├── Image.tsx │ ├── Logo.tsx │ ├── ReleaseNotes.tsx │ ├── Step.tsx │ ├── TutorialSchema.tsx │ ├── Zoom.tsx │ ├── recipes │ │ ├── OneToManySchema.tsx │ │ ├── SlackAppERD.tsx │ │ └── UniqueConstraintSchema.tsx │ ├── seed-tutorial.ts │ └── vs-dark.ts ├── lib │ └── getReleases.ts ├── netlify.toml ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.mdx │ ├── _meta.json │ └── seed │ │ ├── _meta.json │ │ ├── getting-started │ │ ├── _meta.json │ │ ├── installation.mdx │ │ ├── overview.mdx │ │ └── quick-start.mdx │ │ ├── integrations │ │ ├── _meta.json │ │ ├── prisma.mdx │ │ └── supabase.mdx │ │ ├── migrations.mdx │ │ ├── recipes │ │ ├── _meta.json │ │ ├── contraints-and-defaults.mdx │ │ ├── data-pools-connection.mdx │ │ ├── relationships.mdx │ │ └── unique-constraints.mdx │ │ ├── reference │ │ ├── _meta.json │ │ ├── adapters.mdx │ │ ├── cli.mdx │ │ ├── client.mdx │ │ ├── configuration.mdx │ │ └── environment-variables.mdx │ │ ├── release-notes-archive.mdx │ │ └── release-notes.mdx ├── postcss.config.js ├── public │ ├── android-icon-192x192.png │ ├── android-icon-256x256.png │ ├── apple-touch-icon.png │ ├── core-concepts │ │ ├── capture │ │ │ └── snappy-flow.svg │ │ └── deploy │ │ │ ├── deploy-01.png │ │ │ └── deploy-02.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── getting-started │ │ └── quick-start │ │ │ ├── quick-start-01-5.png │ │ │ ├── quick-start-01.png │ │ │ ├── quick-start-01.webp │ │ │ ├── quick-start-02.png │ │ │ ├── quick-start-03.png │ │ │ ├── quick-start-04.png │ │ │ ├── quick-start-05.png │ │ │ ├── quick-start-06.png │ │ │ ├── quick-start-07.png │ │ │ ├── quick-start-08.png │ │ │ ├── quick-start-10.png │ │ │ ├── quick-start-11.png │ │ │ ├── quick-start-12.png │ │ │ ├── quick-start-13.png │ │ │ ├── quick-start-14.png │ │ │ ├── quick-start-15.png │ │ │ ├── quick-start-16.png │ │ │ └── quick-start-17.png │ ├── guides │ │ └── postgresql │ │ │ └── snappy-spilt-milk.svg │ ├── ms-icon-150x150.png │ ├── og.png │ ├── recipes │ │ ├── neon │ │ │ ├── 00-snaplet-snapshot.png │ │ │ ├── 01-snaplet-cli.png │ │ │ ├── 02-neon-conn.png │ │ │ ├── 03-restore-setup.png │ │ │ └── 04-restore-snapshot.png │ │ ├── netlify │ │ │ ├── netlify-access-token1.png │ │ │ ├── netlify-build-logs.png │ │ │ ├── netlify-db-url-new.png │ │ │ ├── netlify-github-token.png │ │ │ ├── netlify-personal-access-token1.png │ │ │ ├── netlify-preview-plugin-logo.png │ │ │ ├── netlify-project-id.png │ │ │ ├── netlify-snaplet-access-token1.png │ │ │ ├── netlify-snaplet-connect-database-success.png │ │ │ ├── netlify-snaplet-connect-database.png │ │ │ ├── netlify-snaplet-enable1.png │ │ │ ├── netlify-snaplet-enable2.png │ │ │ ├── netlify-snaplet-problem.png │ │ │ ├── netlify-snaplet-review-save.png │ │ │ ├── netlify-snaplet-snapshot-success.png │ │ │ └── netlify-snaplet-solution.png │ │ ├── supabase │ │ │ ├── 20_todos.webp │ │ │ ├── connect_to_supabase.webp │ │ │ ├── nextjs_todos.webp │ │ │ ├── onboarding_capture.webp │ │ │ ├── onboarding_start.webp │ │ │ ├── snaplet-supabase-schema-exclude.png │ │ │ ├── snappy-holding-supabase-logo.svg │ │ │ ├── snappy-with-supabase-ball.svg │ │ │ └── supabase_new_project.webp │ │ └── visual-studio-code │ │ │ ├── vscode-01.webp │ │ │ ├── vscode-02.webp │ │ │ ├── vscode-03.webp │ │ │ ├── vscode-04.webp │ │ │ ├── vscode-05.webp │ │ │ └── vscode-06.webp │ ├── security │ │ └── Snaplet-Security.png │ ├── seed │ │ ├── getting-started │ │ │ ├── installation │ │ │ │ └── docs-image-installation.svg │ │ │ ├── overview │ │ │ │ └── Seed_overview.svg │ │ │ └── quick-start │ │ │ │ └── docs-image-quick-start.svg │ │ └── integrations │ │ │ ├── prisma │ │ │ └── Prisma_Docs_image.svg │ │ │ └── supabase │ │ │ └── Supabase_Docs_image.svg │ └── site.webmanifest ├── redirects │ ├── 09_Oct_2023.json │ └── 12_March_2024.json ├── releases │ ├── 20240506160000-v0.97.10.md │ ├── 20240506163727-v0.97.11.md │ ├── 20240509073336-v0.97.12.md │ ├── 20240513130937-v0.97.13.md │ ├── 20240514124032-v0.97.15.md │ ├── 20240516084229-v0.97.16.md │ ├── 20240516163618-v0.97.17.md │ ├── 20240521054140-v0.97.18.md │ ├── 20240521071914-v0.97.19.md │ └── 20240527124203-v0.97.20.md ├── remarkrc.js ├── styles │ └── global.css ├── tailwind.config.js ├── theme.config.tsx ├── tsconfig.json └── tutorials │ └── generate │ ├── .snaplet │ ├── snaplet-client.d.ts │ └── snaplet.d.ts │ └── snaplet.config.ts ├── eslint.config.js ├── flake.lock ├── flake.nix ├── knip.ts ├── package.json ├── packages ├── eslint-config │ ├── package.json │ └── src │ │ ├── index.d.ts │ │ ├── index.js │ │ └── recommended.js ├── seed │ ├── .editorconfig │ ├── LICENSE.md │ ├── README.md │ ├── assets │ │ └── index.ts │ ├── bin │ │ └── cli.js │ ├── e2e │ │ ├── api │ │ │ ├── e2e.api.connect.global.test.ts │ │ │ ├── e2e.api.connect.test.ts │ │ │ ├── e2e.api.connect2.test.ts │ │ │ ├── e2e.api.postgres.test.ts │ │ │ ├── e2e.api.reset.sqlite.test.ts │ │ │ ├── e2e.api.reset.test.ts │ │ │ └── e2e.api.reset2.test.ts │ │ ├── basics │ │ │ ├── e2e.basics.test.ts │ │ │ ├── e2e.basics2.test.ts │ │ │ └── e2e.test.ts │ │ ├── constraints │ │ │ ├── e2e.constraints.test.ts │ │ │ └── e2e.constraints.unique.test.ts │ │ ├── e2e.datatypes.test.ts │ │ ├── e2e.docs.test.ts │ │ ├── e2e.install.test.ts │ │ ├── e2e.paths.test.ts │ │ ├── e2e.postgres.test.ts │ │ ├── fixtures │ │ │ └── install │ │ │ │ ├── .npmrc │ │ │ │ ├── .snaplet │ │ │ │ ├── config.json │ │ │ │ └── dataModel.json │ │ │ │ ├── package.json │ │ │ │ ├── seed.config.ts │ │ │ │ └── seed.ts │ │ ├── keys │ │ │ ├── circular │ │ │ │ ├── e2e.keys.circular.test.ts │ │ │ │ └── e2e.keys.circular2.test.ts │ │ │ ├── e2e.keys.composite.test.ts │ │ │ ├── e2e.keys.test.ts │ │ │ └── postgres │ │ │ │ ├── e2e.keys.postgres.test.ts │ │ │ │ └── e2e.keys.postgres2.test.ts │ │ ├── sequences │ │ │ ├── e2e.sequences.override.test.ts │ │ │ └── e2e.sequences.test.ts │ │ └── transform │ │ │ ├── e2e.transforms.test.ts │ │ │ └── e2e.transforms2.test.ts │ ├── eslint.config.js │ ├── global.d.ts │ ├── package.json │ ├── scripts │ │ ├── patch-defineConfig.mts │ │ ├── postinstall.js │ │ ├── publishToDiscord.mts │ │ ├── publishToDocs.mts │ │ ├── release-alpha.mts │ │ ├── release-latest.mts │ │ └── release-utils.mts │ ├── src │ │ ├── adapters │ │ │ ├── better-sqlite3 │ │ │ │ ├── better-sqlite3.ts │ │ │ │ └── index.ts │ │ │ ├── getAdapter.ts │ │ │ ├── getDatabaseClient.ts │ │ │ ├── index.ts │ │ │ ├── mysql2 │ │ │ │ ├── index.ts │ │ │ │ └── mysql2.ts │ │ │ ├── pg │ │ │ │ ├── index.ts │ │ │ │ └── pg.ts │ │ │ ├── postgres │ │ │ │ ├── index.ts │ │ │ │ └── postgres.ts │ │ │ ├── prisma │ │ │ │ ├── getDialect.ts │ │ │ │ ├── getPrismaDataModel.ts │ │ │ │ ├── index.ts │ │ │ │ ├── patchSeedConfig.ts │ │ │ │ ├── patchUserModels.ts │ │ │ │ └── prisma.ts │ │ │ └── types.ts │ │ ├── cli │ │ │ ├── commands │ │ │ │ ├── config.ts │ │ │ │ ├── generate │ │ │ │ │ ├── computeCodegenContext.ts │ │ │ │ │ ├── generate.ts │ │ │ │ │ └── generateHandler.ts │ │ │ │ ├── init │ │ │ │ │ ├── adapterHandler.ts │ │ │ │ │ ├── generateSeedScriptExample.ts │ │ │ │ │ ├── init.ts │ │ │ │ │ ├── initHandler.ts │ │ │ │ │ ├── installDependencies.ts │ │ │ │ │ ├── isConnected.ts │ │ │ │ │ ├── saveSeedConfig.ts │ │ │ │ │ ├── selectAdapterFromPrompt.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── introspect │ │ │ │ │ ├── introspect.ts │ │ │ │ │ └── introspectHandler.ts │ │ │ │ ├── predict │ │ │ │ │ ├── describeFields.ts │ │ │ │ │ ├── generateExamples.ts │ │ │ │ │ ├── listenForKeyPress.ts │ │ │ │ │ ├── models.ts │ │ │ │ │ └── predictHandler.ts │ │ │ │ ├── sync │ │ │ │ │ ├── sync.ts │ │ │ │ │ └── syncHandler.ts │ │ │ │ └── version.ts │ │ │ ├── index.ts │ │ │ └── lib │ │ │ │ ├── debug.ts │ │ │ │ └── output.ts │ │ ├── config │ │ │ ├── __fixtures__ │ │ │ │ └── configs │ │ │ │ │ └── project │ │ │ │ │ ├── invalid │ │ │ │ │ └── .snaplet │ │ │ │ │ │ └── config.json │ │ │ │ │ └── valid │ │ │ │ │ └── .snaplet │ │ │ │ │ └── config.json │ │ │ ├── dataModelConfig.ts │ │ │ ├── dotSnaplet.ts │ │ │ ├── project │ │ │ │ └── projectConfig.ts │ │ │ ├── seedConfig │ │ │ │ ├── adapterConfig.ts │ │ │ │ ├── aliasConfig.ts │ │ │ │ ├── defineConfig.ts │ │ │ │ ├── fingerprint │ │ │ │ │ ├── config.ts │ │ │ │ │ └── schemas.ts │ │ │ │ ├── seedConfig.ts │ │ │ │ └── selectConfig.ts │ │ │ └── utils.ts │ │ ├── core │ │ │ ├── client │ │ │ │ ├── client.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── codegen │ │ │ │ ├── codegen.ts │ │ │ │ ├── generateClientTypes.ts │ │ │ │ ├── generateConfigTypes.ts │ │ │ │ └── userModels │ │ │ │ │ ├── generateJsonField.ts │ │ │ │ │ └── generateUserModels.ts │ │ │ ├── data │ │ │ │ ├── data.ts │ │ │ │ └── types.ts │ │ │ ├── dataModel │ │ │ │ ├── __fixtures__ │ │ │ │ │ ├── basicPostgresDataModel.json │ │ │ │ │ └── basicSqliteDataModel.json │ │ │ │ ├── aliases.test.ts │ │ │ │ ├── aliases.ts │ │ │ │ ├── dataModel.ts │ │ │ │ ├── select.test.ts │ │ │ │ ├── select.ts │ │ │ │ ├── shouldGenerateFieldValue.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── databaseClient.ts │ │ │ ├── dialect │ │ │ │ ├── generateConfigTypes.ts │ │ │ │ ├── groupParentsChildrenRelations.test.ts │ │ │ │ ├── groupParentsChildrenRelations.ts │ │ │ │ ├── types.ts │ │ │ │ ├── unpackNestedType.ts │ │ │ │ ├── userModels.ts │ │ │ │ └── utils.ts │ │ │ ├── fingerprint │ │ │ │ ├── fingerprint.ts │ │ │ │ └── types.ts │ │ │ ├── plan │ │ │ │ ├── constraints.ts │ │ │ │ ├── plan.ts │ │ │ │ └── types.ts │ │ │ ├── predictions │ │ │ │ ├── computePredictionContext.ts │ │ │ │ ├── shapeExamples │ │ │ │ │ ├── getDataExamples.ts │ │ │ │ │ └── setDataExamples.ts │ │ │ │ ├── shapePredictions │ │ │ │ │ └── getShapePredictions.ts │ │ │ │ ├── types.ts │ │ │ │ ├── utils.test.ts │ │ │ │ └── utils.ts │ │ │ ├── sequences │ │ │ │ └── sequences.ts │ │ │ ├── store │ │ │ │ ├── store.ts │ │ │ │ └── topologicalSort.ts │ │ │ ├── symbols.ts │ │ │ ├── userModels │ │ │ │ ├── encloseValueInArray.ts │ │ │ │ ├── templates │ │ │ │ │ ├── categories │ │ │ │ │ │ ├── bits.ts │ │ │ │ │ │ ├── floats.ts │ │ │ │ │ │ ├── geometry.ts │ │ │ │ │ │ ├── integers.ts │ │ │ │ │ │ ├── strings.test.ts │ │ │ │ │ │ └── strings.ts │ │ │ │ │ ├── codegen.test.ts │ │ │ │ │ ├── codegen.ts │ │ │ │ │ ├── copycat.test.ts │ │ │ │ │ ├── copycat.ts │ │ │ │ │ ├── shapeExtra.ts │ │ │ │ │ ├── testing.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── types.ts │ │ │ │ └── userModels.ts │ │ │ ├── utils.ts │ │ │ └── version.ts │ │ ├── dialects │ │ │ ├── dialects.ts │ │ │ ├── getDialect.ts │ │ │ ├── mysql │ │ │ │ ├── client.ts │ │ │ │ ├── dataModel.ts │ │ │ │ ├── determineShapeFromType.ts │ │ │ │ ├── dialect.ts │ │ │ │ ├── generateClientTypes.ts │ │ │ │ ├── introspect │ │ │ │ │ ├── introspectDatabase.test.ts │ │ │ │ │ ├── introspectDatabase.ts │ │ │ │ │ ├── introspectionToDataModel.ts │ │ │ │ │ ├── queries │ │ │ │ │ │ ├── fetchDatabaseRelationships.test.ts │ │ │ │ │ │ ├── fetchDatabaseRelationships.ts │ │ │ │ │ │ ├── fetchEnums.test.ts │ │ │ │ │ │ ├── fetchEnums.ts │ │ │ │ │ │ ├── fetchPrimaryKeys.test.ts │ │ │ │ │ │ ├── fetchPrimaryKeys.ts │ │ │ │ │ │ ├── fetchSchemas.test.ts │ │ │ │ │ │ ├── fetchSchemas.ts │ │ │ │ │ │ ├── fetchSequences.test.ts │ │ │ │ │ │ ├── fetchSequences.ts │ │ │ │ │ │ ├── fetchTablesAndColumns.test.ts │ │ │ │ │ │ ├── fetchTablesAndColumns.ts │ │ │ │ │ │ ├── fetchUniqueConstraints.test.ts │ │ │ │ │ │ ├── fetchUniqueConstraints.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── store.test.ts │ │ │ │ ├── store.ts │ │ │ │ ├── userModels.test.ts │ │ │ │ ├── userModels.ts │ │ │ │ ├── userModels │ │ │ │ │ └── templates │ │ │ │ │ │ └── geometry.ts │ │ │ │ └── utils.ts │ │ │ ├── postgres │ │ │ │ ├── client.ts │ │ │ │ ├── dataModel.test.ts │ │ │ │ ├── dataModel.ts │ │ │ │ ├── determineShapeFromType.ts │ │ │ │ ├── dialect.ts │ │ │ │ ├── generateClientTypes.ts │ │ │ │ ├── introspect │ │ │ │ │ ├── introspectDatabase.test.ts │ │ │ │ │ ├── introspectDatabase.ts │ │ │ │ │ ├── introspectionToDataModel.ts │ │ │ │ │ ├── queries │ │ │ │ │ │ ├── fetchDatabaseRelationships.test.ts │ │ │ │ │ │ ├── fetchDatabaseRelationships.ts │ │ │ │ │ │ ├── fetchEnums.test.ts │ │ │ │ │ │ ├── fetchEnums.ts │ │ │ │ │ │ ├── fetchPrimaryKeys.test.ts │ │ │ │ │ │ ├── fetchPrimaryKeys.ts │ │ │ │ │ │ ├── fetchSchemas.test.ts │ │ │ │ │ │ ├── fetchSchemas.ts │ │ │ │ │ │ ├── fetchSequences.test.ts │ │ │ │ │ │ ├── fetchSequences.ts │ │ │ │ │ │ ├── fetchTablesAndColumns.test.ts │ │ │ │ │ │ ├── fetchTablesAndColumns.ts │ │ │ │ │ │ ├── fetchUniqueConstraints.test.ts │ │ │ │ │ │ ├── fetchUniqueConstraints.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── store.test.ts │ │ │ │ ├── store.ts │ │ │ │ ├── userModels.test.ts │ │ │ │ ├── userModels.ts │ │ │ │ └── utils.ts │ │ │ └── sqlite │ │ │ │ ├── client.ts │ │ │ │ ├── dataModel.ts │ │ │ │ ├── determineShapeFromType.ts │ │ │ │ ├── dialect.ts │ │ │ │ ├── generateClientTypes.ts │ │ │ │ ├── introspect │ │ │ │ ├── introspectDatabase.test.ts │ │ │ │ ├── introspectDatabase.ts │ │ │ │ ├── introspectionToDataModel.ts │ │ │ │ ├── queries │ │ │ │ │ ├── fetchDatabaseRelationships.test.ts │ │ │ │ │ ├── fetchDatabaseRelationships.ts │ │ │ │ │ ├── fetchPrimaryKeys.test.ts │ │ │ │ │ ├── fetchPrimaryKeys.ts │ │ │ │ │ ├── fetchSequences.test.ts │ │ │ │ │ ├── fetchSequences.ts │ │ │ │ │ ├── fetchTablesAndColumns.test.ts │ │ │ │ │ ├── fetchTablesAndColumns.ts │ │ │ │ │ ├── fetchUniqueConstraints.test.ts │ │ │ │ │ └── fetchUniqueConstraints.ts │ │ │ │ └── types.ts │ │ │ │ ├── store.test.ts │ │ │ │ ├── store.ts │ │ │ │ ├── userModels.ts │ │ │ │ └── utils.ts │ │ ├── index.ts │ │ └── trpc │ │ │ └── shapes.ts │ ├── test │ │ ├── adapters.ts │ │ ├── constants.ts │ │ ├── createDataModelFromSql.ts │ │ ├── createTmpDirectory.ts │ │ ├── debug.ts │ │ ├── mysql │ │ │ ├── fixtures │ │ │ │ └── snaplet_schema.sql │ │ │ └── mysql │ │ │ │ ├── createTestDatabase.ts │ │ │ │ └── index.ts │ │ ├── postgres │ │ │ ├── fixtures │ │ │ │ └── snaplet_schema.sql │ │ │ └── postgres │ │ │ │ ├── createTestDatabase.ts │ │ │ │ ├── createTestRole.ts │ │ │ │ └── index.ts │ │ ├── runCli.ts │ │ ├── runSeedScript.ts │ │ ├── setupProject.ts │ │ ├── sqlite │ │ │ ├── better-sqlite3 │ │ │ │ ├── createTestDatabase.ts │ │ │ │ └── index.ts │ │ │ └── fixtures │ │ │ │ └── chinook.db │ │ └── types.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.ts └── tsconfig │ ├── package.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── turbo.json └── vitest.workspace.ts /.envrc: -------------------------------------------------------------------------------- 1 | use flake . --impure -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: Install 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Get pnpm store directory 7 | shell: devenv {0} 8 | run: | 9 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 10 | 11 | - name: Setup pnpm cache 12 | uses: buildjet/cache@v4 13 | with: 14 | path: ${{ env.STORE_PATH }} 15 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 16 | restore-keys: | 17 | ${{ runner.os }}-pnpm-store- 18 | 19 | - name: Install dependencies 20 | shell: devenv {0} 21 | run: pnpm install -------------------------------------------------------------------------------- /.github/actions/publish/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | inputs: 4 | channel: 5 | description: 'The channel to publish to (alpha | latest)' 6 | required: true 7 | token: 8 | description: 'The NPM token to use for publishing' 9 | required: true 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: Set short-sha 15 | id: short-sha 16 | shell: devenv {0} 17 | run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 18 | 19 | - name: Setup npmrc auth for publish 20 | shell: devenv {0} 21 | run: | 22 | echo '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}' >> .npmrc && \ 23 | echo "registry=https://registry.npmjs.org/" >> .npmrc && \ 24 | echo "always-auth=true" >> .npmrc 25 | 26 | - name: Publish package 27 | shell: devenv {0} 28 | run: pnpm -F @snaplet/seed run release:${{ inputs.channel }} 29 | env: 30 | NODE_AUTH_TOKEN: ${{ inputs.token }} 31 | GIT_HASH: ${{ steps.short-sha.outputs.sha_short }} 32 | -------------------------------------------------------------------------------- /.github/actions/setup-nix/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Nix 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Install Nix 7 | uses: nixbuild/nix-quick-install-action@v26 8 | 9 | - name: Restore and cache Nix store 10 | uses: avallete/cache-nix-action@v0.0.1 11 | with: 12 | primary-key: cache-${{ runner.os }}-nix-store-${{ hashFiles('**/*.nix') }} 13 | restore-prefixes-first-match: cache-${{ runner.os }}-nix-store- 14 | 15 | - name: Create a custom shell for devenv 16 | shell: bash 17 | run: | 18 | mkdir -p $HOME/.local/bin 19 | echo "#!/usr/bin/env bash" > $HOME/.local/bin/devenv 20 | echo "nix develop --impure --command bash \"\$1\"" >> $HOME/.local/bin/devenv 21 | chmod +x $HOME/.local/bin/devenv 22 | 23 | - name: Build the devenv shell 24 | shell: devenv {0} 25 | run: echo 🚀 26 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Setup Nix 7 | uses: ./.github/actions/setup-nix 8 | 9 | - name: Install dependencies 10 | uses: ./.github/actions/install 11 | -------------------------------------------------------------------------------- /.github/docker/Dockerfile.mysql: -------------------------------------------------------------------------------- 1 | FROM mysql:8 2 | 3 | CMD ["mysqld", "--wait_timeout=28800", "--interactive_timeout=28800"] 4 | -------------------------------------------------------------------------------- /.github/docker/Dockerfile.postgres: -------------------------------------------------------------------------------- 1 | FROM postgres:16 2 | 3 | # Use a shell script to modify config and start PostgreSQL 4 | 5 | CMD ["postgres", "-c", "max_connections=300"] 6 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "group:allNonMajor" 6 | ], 7 | "automerge": true, 8 | "timezone": "Europe/Paris", 9 | "schedule": ["after 9am and before 10am"], 10 | "postUpdateOptions": ["pnpmDedupe"] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/create-draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Create draft release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | concurrency: 9 | group: create-draft-release-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | create-draft-release: 14 | runs-on: buildjet-4vcpu-ubuntu-2204 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Create draft release 20 | uses: softprops/action-gh-release@v2 21 | with: 22 | draft: true 23 | body: | 24 | ### Breaking changes 🚨 25 | * Oops, we did it again! 26 | 27 | ### New Features 🎉 28 | * A very useful feature 29 | 30 | ### Bugfixes 🐛 31 | * One bug fixed 32 | * Another bug fixed! 33 | 34 | ### Improvements 🛠️ 35 | * It's so fast now! -------------------------------------------------------------------------------- /.github/workflows/release-alpha.yml: -------------------------------------------------------------------------------- 1 | name: Release Alpha 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: release-npm 10 | cancel-in-progress: false 11 | 12 | defaults: 13 | run: 14 | shell: devenv {0} 15 | 16 | env: 17 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 18 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 19 | 20 | jobs: 21 | build-and-publish: 22 | runs-on: buildjet-4vcpu-ubuntu-2204 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup 28 | uses: ./.github/actions/setup 29 | 30 | - name: Publish 31 | uses: ./.github/actions/publish 32 | with: 33 | channel: alpha 34 | token: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/release-latest.yml: -------------------------------------------------------------------------------- 1 | name: Release Latest 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | concurrency: 8 | group: release-npm 9 | cancel-in-progress: false 10 | 11 | defaults: 12 | run: 13 | shell: devenv {0} 14 | 15 | env: 16 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 17 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 18 | 19 | jobs: 20 | build-and-publish: 21 | runs-on: buildjet-4vcpu-ubuntu-2204 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup 27 | uses: ./.github/actions/setup 28 | 29 | - name: Publish package 30 | uses: ./.github/actions/publish 31 | with: 32 | channel: latest 33 | token: ${{ secrets.NPM_TOKEN }} 34 | 35 | - name: Publish release note to docs.snaplet.dev 36 | run: pnpm -F @snaplet/seed run publish:docs 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GH_PAT_COMMIT }} 39 | RELEASE_TAG: ${{ github.event.release.tag_name }} 40 | 41 | - name: Publish release note to Discord 42 | run: pnpm -F @snaplet/seed run publish:discord 43 | env: 44 | DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} 45 | GITHUB_TOKEN: ${{ secrets.GH_PAT_COMMIT }} 46 | RELEASE_TAG: ${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /.github/workflows/tag-release.yml: -------------------------------------------------------------------------------- 1 | name: Tag release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - 'packages/seed/package.json' 10 | 11 | concurrency: 12 | group: tag-release-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | tag-release: 17 | runs-on: buildjet-4vcpu-ubuntu-2204 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Check package version 23 | id: cpv 24 | uses: PostHog/check-package-version@v2 25 | with: 26 | path: packages/seed 27 | 28 | - name: Push a new tag on git repo 29 | if: steps.cpv.outputs.is-new-version == 'true' 30 | uses: Klemensas/action-autotag@stable 31 | env: 32 | GITHUB_TOKEN: "${{ secrets.GH_PAT_COMMIT }}" 33 | with: 34 | package_root: "packages/seed" 35 | tag_prefix: "v" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "orta.vscode-twoslash-queries", 5 | "yoavbls.pretty-ts-errors", 6 | "vitest.explorer" 7 | ], 8 | "unwantedRecommendations": [ 9 | "esbenp.prettier-vscode" 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | }, 5 | "editor.formatOnSave": false, 6 | "eslint.useFlatConfig": true, 7 | "eslint.workingDirectories": [ 8 | { 9 | "pattern": "./packages/*" 10 | }, 11 | ], 12 | "typescript.disableAutomaticTypeAcquisition": true, 13 | "typescript.tsdk": "node_modules/typescript/lib", 14 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "options": { 4 | "shell": { 5 | "executable": "nix", 6 | "args": ["develop", "--impure", "--command", "bash", "-c"] 7 | } 8 | }, 9 | "tasks": [ 10 | { 11 | "label": "dev:env", 12 | "type": "shell", 13 | "command": "devenv up", 14 | "isBackground": true, 15 | "presentation": { 16 | "reveal": "silent", 17 | "panel": "dedicated", 18 | }, 19 | }, 20 | { 21 | "label": "dev:type-check", 22 | "type": "shell", 23 | "command": "pnpm type-check --watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "always", 27 | "panel": "dedicated", 28 | "group": "dev" 29 | }, 30 | }, 31 | { 32 | "label": "dev:api", 33 | "type": "shell", 34 | "command": "pnpm -F api dev", 35 | "isBackground": true, 36 | "presentation": { 37 | "reveal": "always", 38 | "panel": "dedicated", 39 | "group": "dev" 40 | }, 41 | }, 42 | { 43 | "label": "dev:web", 44 | "type": "shell", 45 | "command": "pnpm -F web dev", 46 | "isBackground": true, 47 | "presentation": { 48 | "reveal": "always", 49 | "panel": "dedicated", 50 | "group": "dev" 51 | }, 52 | }, 53 | { 54 | "label": "dev", 55 | "dependsOn": [ 56 | "dev:env", 57 | "dev:type-check", 58 | "dev:api", 59 | "dev:web" 60 | ], 61 | "problemMatcher": [] 62 | }, 63 | ] 64 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Snaplet, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE_WORKFLOW.md: -------------------------------------------------------------------------------- 1 | # Release Workflow 2 | 3 | 1. Update the version in `packages/seed/package.json` 4 | 2. *A new tag will be added in the repository* 5 | 3. *A new draft release will be created* 6 | 4. Edit the draft release with your notes in the form: 7 | ```md 8 | ### Breaking changes 🚨 9 | * Oops, we did it again! 10 | 11 | ### New Features 🎉 12 | * A very useful feature 13 | 14 | ### Bugfixes 🐛 15 | * One bug fixed 16 | * Another bug fixed! 17 | 18 | ### Improvements 🛠️ 19 | * It's so fast now! 20 | ``` 21 | It's **super important** that you use the **level 3 `###`** for titles, to be compatible with our documentation layout 22 | 23 | 5. Click on the `publish` button to make the release live 24 | 6. *The release will be automatically published to our documentation and on Discord* 25 | 26 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .DS_Store -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix="" -------------------------------------------------------------------------------- /docs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "unifiedjs.vscode-mdx" 4 | ] 5 | } -------------------------------------------------------------------------------- /docs/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "unifiedjs.vscode-remark", 4 | "javascript.format.enable": false, 5 | "typescript.format.enable": false, 6 | "typescript.tsdk": "node_modules/typescript/lib", 7 | "[mdx]": { 8 | "editor.defaultFormatter": "unifiedjs.vscode-mdx" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Simple Browser", 6 | "command": "${input:openSimpleBrowser}", 7 | "problemMatcher": [] 8 | } 9 | ], 10 | "inputs": [ 11 | { 12 | "id": "openSimpleBrowser", 13 | "type": "command", 14 | "command": "simpleBrowser.show", 15 | "args": ["http://localhost:3000"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### Prerequisites 2 | 3 | Install `brew`, `git`, `pnpm` and `Node.js` : 4 | 5 | ### Installation 6 | 7 | ```bash 8 | cd docs 9 | pnpm install 10 | ``` 11 | 12 | ### Run the project 13 | 14 | ```bash 15 | pnpm dev 16 | # Go to http://localhost:3000 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/components/Image.tsx: -------------------------------------------------------------------------------- 1 | import NextImage, { type ImageProps } from "next/image"; 2 | import Zoom from "./Zoom"; 3 | 4 | export function Image(props: { src: string; alt: string; zoom?: boolean }) { 5 | const ImageComponent = props.zoom ? Zoom : NextImage; 6 | 7 | return ( 8 |
9 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /docs/components/ReleaseNotes.tsx: -------------------------------------------------------------------------------- 1 | import { useData } from "nextra/data" 2 | import dynamic from "next/dynamic" 3 | 4 | function ReleaseNote(props: { filename: string }) { 5 | const Content = dynamic(() => import(`../releases/${props.filename}`)) 6 | 7 | return ( 8 | 9 | ) 10 | } 11 | 12 | export function ReleaseNotes() { 13 | const { releases } = useData() as { releases: Array<{ filename: string }>} 14 | 15 | return ( 16 | <> 17 | { releases.map((release, index) => ) } 18 | 19 | ) 20 | } -------------------------------------------------------------------------------- /docs/components/Step.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Zoom } from "./Zoom"; 3 | 4 | export function Step(props: { 5 | children: React.ReactNode; 6 | image?: { src: string; alt: string }; 7 | }) { 8 | if (!props.image) { 9 | return props.children; 10 | } 11 | 12 | return ( 13 |
14 |
{props.children}
15 |
16 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /docs/components/recipes/SlackAppERD.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react"; 2 | import { useTheme } from "nextra-theme-docs"; 3 | 4 | export const SlackAppERD = () => { 5 | const { resolvedTheme } = useTheme(); 6 | const [iframeSrc, setIframeSrc] = useState(''); 7 | 8 | useEffect(() => { 9 | // This code now runs only on the client side 10 | if (resolvedTheme === "dark") { 11 | setIframeSrc("https://dbdiagram.io/e/66460034f84ecd1d225b7d68/66460063f84ecd1d225b82f2"); 12 | } else { 13 | setIframeSrc("https://dbdiagram.io/e/6644dfeff84ecd1d2244fbde/6644e883f84ecd1d2245ac3c"); 14 | } 15 | }, [resolvedTheme]); 16 | 17 | return ( 18 | 24 | ); 25 | }; -------------------------------------------------------------------------------- /docs/lib/getReleases.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import fs from "fs/promises" 3 | 4 | export async function getReleases() { 5 | const releasesDirectory = path.join(process.cwd(), 'releases') 6 | const files = await fs.readdir(releasesDirectory) 7 | 8 | const releases = files.map(async (file) => { 9 | const filePath = path.join(releasesDirectory, file) 10 | const filePathParsed = path.parse(filePath) 11 | return { 12 | filename: filePathParsed.base 13 | } 14 | }) 15 | 16 | return (await Promise.all(releases)).sort(((a, b) => a.filename.localeCompare(b.filename))).reverse() 17 | } -------------------------------------------------------------------------------- /docs/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | base = "docs" 3 | publish = ".next" 4 | command = "pnpm install && pnpm --filter @snaplet/documentation build" 5 | [build.environment] 6 | NODE_VERSION = "20.12.2" 7 | [[plugins]] 8 | package = "@netlify/plugin-nextjs" -------------------------------------------------------------------------------- /docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /docs/next.config.js: -------------------------------------------------------------------------------- 1 | const { remarkCodeHike } = require("@code-hike/mdx"); 2 | 3 | const withNextra = require("nextra")({ 4 | theme: "nextra-theme-docs", 5 | themeConfig: "./theme.config.tsx", 6 | unstable_underscoreMeta: true, 7 | mdxOptions: { 8 | remarkPlugins: [ 9 | [remarkCodeHike, { theme: "github-from-css", showCopyButton: true }], 10 | ], 11 | }, 12 | }); 13 | 14 | /** 15 | * @param {Record} json 16 | * ```ts 17 | * const input = convertsJSONToRedirects({ "/": "/redirected" }) 18 | * 19 | * // [{ source: "/", destination: "/redirected", permanent: true }] 20 | * console.log(input) 21 | * ``` 22 | */ 23 | const convertJSONToRedirects = (json) => { 24 | return Object.entries(json).map((j) => ({ 25 | source: j[0], 26 | destination: j[1], 27 | permanent: true, 28 | })); 29 | }; 30 | 31 | /** import('next').Config */ 32 | module.exports = withNextra({ 33 | transpilePackages: ["monaco-editor"], 34 | redirects() { 35 | return [ 36 | { 37 | source: "/", 38 | destination: "/seed", 39 | permanent: false, 40 | }, 41 | { 42 | source: "/seed", 43 | destination: "/seed/getting-started/overview", 44 | permanent: false, 45 | }, 46 | // redirects for when we need to support deprecated links used in CLI 0.63.6 and below 47 | ...convertJSONToRedirects(require("./redirects/09_Oct_2023.json")), 48 | // redirects for when we split the docs into snapshots and seed 49 | ...convertJSONToRedirects(require("./redirects/12_March_2024.json")), 50 | ]; 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@snaplet/documentation", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "format": "remark . --output", 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "packageManager": "pnpm@8.9.0", 16 | "dependencies": { 17 | "@code-hike/mdx": "0.9.0", 18 | "next": "14.1.4", 19 | "nextra": "2.13.4", 20 | "nextra-theme-docs": "2.13.4", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0" 23 | }, 24 | "devDependencies": { 25 | "@netlify/plugin-nextjs": "5.6.0", 26 | "@types/node": "20.12.3", 27 | "@types/react": "18.2.74", 28 | "autoprefixer": "10.4.19", 29 | "postcss": "8.4.38", 30 | "remark-cli": "12.0.0", 31 | "tailwindcss": "3.4.3", 32 | "typescript": "5.4.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/pages/_app.mdx: -------------------------------------------------------------------------------- 1 | import "@code-hike/mdx/dist/index.css"; 2 | import "../styles/global.css"; 3 | 4 | export default function App({ Component, pageProps }) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /docs/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": { 3 | "display": "hidden", 4 | "type": "page" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/pages/seed/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "getting-started": "Getting Started", 3 | "recipes": "Recipes", 4 | "integrations": "Integrations", 5 | "reference": "Reference", 6 | "release-notes": "Release Notes", 7 | "release-notes-archive": { 8 | "display": "hidden" 9 | }, 10 | "migrations": "Migrations" 11 | } 12 | -------------------------------------------------------------------------------- /docs/pages/seed/getting-started/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "overview": "Overview", 3 | "installation": "Installation", 4 | "quick-start": "Quick Start" 5 | } 6 | -------------------------------------------------------------------------------- /docs/pages/seed/integrations/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "prisma": "Prisma", 3 | "supabase": "Supabase" 4 | } 5 | -------------------------------------------------------------------------------- /docs/pages/seed/recipes/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "relationships": "Relationships", 3 | "data-pools-connection": "Data pools connection", 4 | "contraints-and-defaults": "Constraints and DEFAULT", 5 | "unique-constraints": "Unique constraints" 6 | } 7 | -------------------------------------------------------------------------------- /docs/pages/seed/reference/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "adapters": "Adapters", 3 | "configuration": "Configuration", 4 | "environment-variables": "Environment Variables", 5 | "cli": "Seed CLI", 6 | "client": "Seed Client API" 7 | } 8 | -------------------------------------------------------------------------------- /docs/pages/seed/reference/cli.mdx: -------------------------------------------------------------------------------- 1 | # Seed CLI 2 | 3 | You can use the Command-Line Interface (CLI) provided by `@snaplet/seed` to manage your Seed Client from a terminal window. 4 | 5 | ## **init** 6 | 7 | The `npx @snaplet/seed init` command is used to initialize Seed locally for your project. 8 | 9 | **Arguments** 10 | 11 | - `` - Directory path to initialize Snaplet Seed in. 12 | 13 | **Usage** 14 | 15 | Initialize Seed in the current directory: 16 | ```bash >_ terminal 17 | npx @snaplet/seed init 18 | ``` 19 | 20 | Initialize Seed in a specific directory: 21 | ```bash >_ terminal 22 | npx @snaplet/seed init prisma/seed 23 | ``` 24 | If you initialize Seed in a specific directory, you will have to specify the [`--config`](#common-options) option in the other commands. 25 | 26 | ## **login** 27 | 28 | The `npx @snaplet/seed login` command is used to log into your Snaplet account. 29 | 30 | **Options** 31 | 32 | - `--access-token` - Snaplet Cloud access token to use for login, you can obtain one at https://app.snaplet.dev/access-tokens. 33 | 34 | **Usage** 35 | 36 | Interactive login: 37 | ```bash >_ terminal 38 | npx @snaplet/seed login 39 | ``` 40 | 41 | Login with access token: 42 | ```bash >_ terminal 43 | npx @snaplet/seed login --access-token 44 | ``` 45 | 46 | ## **sync** 47 | 48 | The `npx @snaplet/seed sync` command is used to synchronize your database schema with Seed Client. 49 | 50 | **Usage** 51 | 52 | ```bash >_ terminal 53 | npx @snaplet/seed sync 54 | ``` 55 | 56 | ## **generate** 57 | 58 | The `npx @snaplet/seed generate` command is used to generate the necessary assets for Seed Client. 59 | 60 | **Usage** 61 | 62 | ```bash >_ terminal 63 | npx @snaplet/seed generate 64 | ``` 65 | 66 | ## Common options 67 | 68 | - `--config ` - Path to the config file. 69 | 70 | **Usage** 71 | 72 | ```bash >_ terminal 73 | npx @snaplet/seed --config prisma/seed/seed.config.ts sync 74 | ``` 75 | 76 | ## **link** 77 | 78 | The `npx @snaplet/seed link` command is used to link your local directory to a Snaplet Project. 79 | 80 | **Usage** 81 | 82 | ```bash >_ terminal 83 | npx @snaplet/seed link 84 | ``` 85 | 86 | ## Global options 87 | 88 | - `--help` - Show help. 89 | - `--version` - Show version number. 90 | -------------------------------------------------------------------------------- /docs/pages/seed/reference/environment-variables.mdx: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | 3 | You can use these environment variables to customize the Seed CLI behavior. 4 | 5 | | Name | Description | 6 | | :---------------------------- | :--------------------------------------------------------------------------------------| 7 | | `OPENAI_API_KEY` | Use an OpenAI API key to generate examples for text-based entries. | 8 | | `GROQ_API_KEY` | Use a GROQ API key to generate examples for text-based entries. | 9 | | `AI_MODEL_NAME` | Specify the AI model name. Example: `gpt-4-mini,llama-3.1-8b-instant` | 10 | | `AI_CONCURRENCY` | Specify the number of concurrent requests to make. Default for OpeanAI is 5 and Groq 1 | 11 | 12 | -------------------------------------------------------------------------------- /docs/pages/seed/release-notes.mdx: -------------------------------------------------------------------------------- 1 | import { Step } from '../../components/Step' 2 | import { ReleaseNotes } from "../../components/ReleaseNotes" 3 | import { getReleases } from '../../lib/getReleases' 4 | import { Steps } from "nextra/components" 5 | 6 | export async function getStaticProps() { 7 | return { 8 | props: { 9 | ssg: { 10 | releases: await getReleases() 11 | } 12 | }, 13 | } 14 | } 15 | 16 | # Release notes 17 | 18 | *The latest Snaplet product updates from the Snaplet team.* 19 | 20 | 21 | 22 | --- 23 | 24 | **Previous releases can be found [here](/seed/release-notes-archive)** -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /docs/public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/android-icon-192x192.png -------------------------------------------------------------------------------- /docs/public/android-icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/android-icon-256x256.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/core-concepts/deploy/deploy-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/core-concepts/deploy/deploy-01.png -------------------------------------------------------------------------------- /docs/public/core-concepts/deploy/deploy-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/core-concepts/deploy/deploy-02.png -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-01-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-01-5.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-01.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-01.webp -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-02.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-03.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-04.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-05.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-06.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-07.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-08.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-10.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-11.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-12.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-13.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-14.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-15.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-16.png -------------------------------------------------------------------------------- /docs/public/getting-started/quick-start/quick-start-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/getting-started/quick-start/quick-start-17.png -------------------------------------------------------------------------------- /docs/public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /docs/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/og.png -------------------------------------------------------------------------------- /docs/public/recipes/neon/00-snaplet-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/neon/00-snaplet-snapshot.png -------------------------------------------------------------------------------- /docs/public/recipes/neon/01-snaplet-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/neon/01-snaplet-cli.png -------------------------------------------------------------------------------- /docs/public/recipes/neon/02-neon-conn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/neon/02-neon-conn.png -------------------------------------------------------------------------------- /docs/public/recipes/neon/03-restore-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/neon/03-restore-setup.png -------------------------------------------------------------------------------- /docs/public/recipes/neon/04-restore-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/neon/04-restore-snapshot.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-access-token1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-access-token1.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-build-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-build-logs.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-db-url-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-db-url-new.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-github-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-github-token.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-personal-access-token1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-personal-access-token1.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-preview-plugin-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-preview-plugin-logo.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-project-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-project-id.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-access-token1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-access-token1.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-connect-database-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-connect-database-success.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-connect-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-connect-database.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-enable1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-enable1.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-enable2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-enable2.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-problem.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-review-save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-review-save.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-snapshot-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-snapshot-success.png -------------------------------------------------------------------------------- /docs/public/recipes/netlify/netlify-snaplet-solution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/netlify/netlify-snaplet-solution.png -------------------------------------------------------------------------------- /docs/public/recipes/supabase/20_todos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/supabase/20_todos.webp -------------------------------------------------------------------------------- /docs/public/recipes/supabase/connect_to_supabase.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/supabase/connect_to_supabase.webp -------------------------------------------------------------------------------- /docs/public/recipes/supabase/nextjs_todos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/supabase/nextjs_todos.webp -------------------------------------------------------------------------------- /docs/public/recipes/supabase/onboarding_capture.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/supabase/onboarding_capture.webp -------------------------------------------------------------------------------- /docs/public/recipes/supabase/onboarding_start.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/supabase/onboarding_start.webp -------------------------------------------------------------------------------- /docs/public/recipes/supabase/snaplet-supabase-schema-exclude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/supabase/snaplet-supabase-schema-exclude.png -------------------------------------------------------------------------------- /docs/public/recipes/supabase/supabase_new_project.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/supabase/supabase_new_project.webp -------------------------------------------------------------------------------- /docs/public/recipes/visual-studio-code/vscode-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/visual-studio-code/vscode-01.webp -------------------------------------------------------------------------------- /docs/public/recipes/visual-studio-code/vscode-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/visual-studio-code/vscode-02.webp -------------------------------------------------------------------------------- /docs/public/recipes/visual-studio-code/vscode-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/visual-studio-code/vscode-03.webp -------------------------------------------------------------------------------- /docs/public/recipes/visual-studio-code/vscode-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/visual-studio-code/vscode-04.webp -------------------------------------------------------------------------------- /docs/public/recipes/visual-studio-code/vscode-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/visual-studio-code/vscode-05.webp -------------------------------------------------------------------------------- /docs/public/recipes/visual-studio-code/vscode-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/recipes/visual-studio-code/vscode-06.webp -------------------------------------------------------------------------------- /docs/public/security/Snaplet-Security.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/docs/public/security/Snaplet-Security.png -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Snaplet", 3 | "short_name": "Snaplet", 4 | "icons": [ 5 | { 6 | "src": "/android-icon-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-icon-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#b5bdf6", 17 | "background_color": "#b5bdf6", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /docs/redirects/09_Oct_2023.json: -------------------------------------------------------------------------------- 1 | { 2 | "/references/connection-strings": "/guides/postgresql#connection-strings", 3 | "/references/connection-strings/#troubleshooting": "/guides/postgresql#troubleshooting-connection-strings", 4 | "/references/configuration-files": "/reference/configuration", 5 | "/getting-started/start-here": "/getting-started/overview", 6 | "/getting-started/data-operations": "/core-concepts/reference/configuration", 7 | "/tutorials/supabase-clone-environments#step-6-restore-the-data-target": "/recipes/supabase#6-restore-the-data-target", 8 | "/references/data-operations/generate": "/reference/configuration#generate", 9 | "/references/data-operations/exclude": "/reference/configuration#select", 10 | "/references/data-operations/transform": "/reference/configuration#transform", 11 | "/references/data-operations/reduce": "/reference/configuration#subset", 12 | "/references/data-operations/introspect": "/reference/configuration#introspect", 13 | "/tutorials/neon": "/recipes/neon", 14 | "/tutorials/prisma-seed": "/recipes/prisma", 15 | "/references/preview-databases": "/core-concepts/deploy#preview-databases-on-snaplet-cloud", 16 | "/guides/netlify-preview-plugin": "/recipes/netlify", 17 | "/getting-started/sharing": "/getting-started/overview", 18 | "/tutorials/supabase-clone-environments": "/recipes/supabase", 19 | "/getting-started/quick-start/guides/postgresql": "/guides/postgresql", 20 | "/getting-started/quick-start": "/getting-started/overview", 21 | "/getting-started/configuration": "/getting-started/overview", 22 | "/getting-started/generate": "/getting-started/quick-start/generate", 23 | "/getting-started/restoring": "/getting-started/overview", 24 | "/configuration/snaplet-config-file": "/reference/configuration", 25 | "/core-concepts/generate": "/core-concepts/seed", 26 | "/getting-started/quick-start/generate": "/getting-started/quick-start/seed", 27 | "/migrations/generate": "/migrations/seed", 28 | "/references/snapshot": "/core-concepts/capture", 29 | "/core-concepts/reference/configuration": "/reference/configuration" 30 | } 31 | -------------------------------------------------------------------------------- /docs/redirects/12_March_2024.json: -------------------------------------------------------------------------------- 1 | { 2 | "/getting-started/overview": "/seed/getting-started/overview", 3 | "/getting-started/installation": "/seed/getting-started/installation", 4 | "/references/configuration-files": "/seed/reference/configuration", 5 | "/getting-started/quick-start/seed": "/seed/getting-started/quick-start", 6 | "/getting-started/quick-start/snapshot": "/snapshot/getting-started/quick-start", 7 | "/core-concepts/why-snaplet": "/seed/getting-started/quick-start", 8 | "/core-concepts/seed": "/seed/reference/client", 9 | "/seed/core-concepts": "/seed/reference/client", 10 | "/core-concepts/capture": "/snapshot/core-concepts/capture", 11 | "/core-concepts/restore": "/snapshot/core-concepts/restore", 12 | "/core-concepts/share": "/snapshot/core-concepts/share", 13 | "/core-concepts/deploy": "/snapshot/core-concepts/deploy", 14 | "/core-concepts/snaplet-dev-flow": "/snapshot/core-concepts/snaplet-dev-flow", 15 | "/guides/postgresql": "/snapshot/guides/postgresql", 16 | "/guides/self-hosting": "/snapshot/guides/self-hosting", 17 | "/recipes/aws": "/snapshot/recipes/aws", 18 | "/recipes/github-action": "/snapshot/recipes/github-action", 19 | "/recipes/neon": "/snapshot/recipes/neon", 20 | "/recipes/netlify": "/snapshot/recipes/netlify", 21 | "/recipes/prisma": "/seed/recipes/prisma", 22 | "/recipes/supabase": "/seed/recipes/supabase", 23 | "/recipes/vercel": "/snapshot/recipes/vercel", 24 | "/migrations/seed": "/seed/migrations", 25 | "/migrations/snapshot": "/snapshot/migrations", 26 | "/reference/connection-strings": "/seed/reference/connection-strings", 27 | "/seed/recipes/prisma": "/seed/integrations/prisma", 28 | "/seed/recipes/supabase": "/seed/integrations/supabase", 29 | "/reference/cli": "/snapshot/reference/cli" 30 | } 31 | -------------------------------------------------------------------------------- /docs/releases/20240506160000-v0.97.10.md: -------------------------------------------------------------------------------- 1 | ## v0.97.10 - 6 May 2024 2 | 3 | ### Bugfixes 🐛 4 | * For databases with no fields that can be used with Snaplet AI (e.g. if there were no text fields in the db, or if all the fields had non-composite unique constraints), users would be left waiting indefinitely for Snaplet AI to complete. 5 | * Fixes to how we identify unique users in our analytics -------------------------------------------------------------------------------- /docs/releases/20240506163727-v0.97.11.md: -------------------------------------------------------------------------------- 1 | ## v0.97.11 - 6 May 2024 2 | 3 | ### Bugfix 🐛 4 | * Correctly escape the configuration path on Windows when generating assets -------------------------------------------------------------------------------- /docs/releases/20240509073336-v0.97.12.md: -------------------------------------------------------------------------------- 1 | ## v0.97.12 - 9 May 2024 2 | 3 | ### Improvements 🛠️ 4 | * In our `init` and `sync` commands, we now show total progress for all Snaplet AI jobs (we previously were only able to show progress some kinds of Snaplet AI jobs). We can pick up when the jobs have started sooner and let you opt to skip waiting for all jobs rather than only some of them. This way you have a more complete picture of what has happened, and have more control from start to end. -------------------------------------------------------------------------------- /docs/releases/20240513130937-v0.97.13.md: -------------------------------------------------------------------------------- 1 | ## v0.97.13 - 13 May 2024 2 | 3 | ### Improvements 🛠️ 4 | * The `sync` workflow skips the AI task when the data model has no changes -------------------------------------------------------------------------------- /docs/releases/20240514124032-v0.97.15.md: -------------------------------------------------------------------------------- 1 | ## v0.97.15 - 14 May 2024 2 | 3 | ### Bugfixes 🐛 4 | * Fix an aliasing bug for child relationships on prisma projects 5 | -------------------------------------------------------------------------------- /docs/releases/20240516084229-v0.97.16.md: -------------------------------------------------------------------------------- 1 | ## v0.97.16 - 16 May 2024 2 | 3 | ### Bugfixes 🐛 4 | * Bug where global connect and plan connect option weren't properly merged togethers -------------------------------------------------------------------------------- /docs/releases/20240516163618-v0.97.17.md: -------------------------------------------------------------------------------- 1 | ## v0.97.17 - 16 May 2024 2 | 3 | ### New Features 🎉 4 | * If you use a custom configuration path, for example: `prisma/seed/seed.config.ts`, you can save it to your `package.json` to avoid using the `--config` option for each CLI command. You can read more in the [Configuration Reference](https://docs.snaplet.dev/seed/reference/configuration#config-location). -------------------------------------------------------------------------------- /docs/releases/20240521054140-v0.97.18.md: -------------------------------------------------------------------------------- 1 | ## v0.97.18 - 21 May 2024 2 | 3 | ### Bugfixes 🐛 4 | * Added compatibility for Vitess based MySQL databases (Planetscale) 5 | -------------------------------------------------------------------------------- /docs/releases/20240521071914-v0.97.19.md: -------------------------------------------------------------------------------- 1 | ## v0.97.19 - 21 May 2024 2 | 3 | ### Bugfixes 🐛 4 | * Compatibility with `vitest` - `@snaplet/seed` was not import-able in vitest -------------------------------------------------------------------------------- /docs/releases/20240527124203-v0.97.20.md: -------------------------------------------------------------------------------- 1 | ## v0.97.20 - 27 May 2024 2 | 3 | ### Bugfixes 🐛 4 | * Only consider unique indexes when introspecting SQLite databases 5 | -------------------------------------------------------------------------------- /docs/remarkrc.js: -------------------------------------------------------------------------------- 1 | import remarkMdx from "remark-mdx"; 2 | 3 | const remarkConfig = { 4 | plugins: [remarkMdx], 5 | }; 6 | 7 | export default remarkConfig; 8 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx,md,mdx}", 5 | "./pages/**/*.{js,ts,jsx,tsx,md,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,md,mdx}", 7 | 8 | // Or if using `src` directory: 9 | "./src/**/*.{js,ts,jsx,tsx,md,mdx}", 10 | ], 11 | theme: { 12 | extend: {}, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules", "tutorials"] 19 | } 20 | -------------------------------------------------------------------------------- /docs/tutorials/generate/snaplet.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "snaplet"; 2 | import { copycat } from "@snaplet/copycat"; 3 | 4 | export default defineConfig({ 5 | generate: { 6 | plan({ snaplet }) { 7 | return snaplet.Post({ 8 | data: { 9 | title: "There is a lot of snow around here!", 10 | User: { 11 | data: { 12 | email: ({ seed }) => copycat.email(seed, { domain: "acme.org" }), 13 | }, 14 | }, 15 | Comment: { 16 | count: 3, 17 | }, 18 | }, 19 | }); 20 | }, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, recommended } from "@snaplet/eslint-config"; 2 | 3 | export default defineConfig([ 4 | { 5 | ignores: ["**/{.devenv,.direnv,apps,node_modules,packages,docs}"], 6 | }, 7 | ...recommended, 8 | ]); 9 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | systems.url = "github:nix-systems/default"; 5 | devenv.url = "github:cachix/devenv"; 6 | devenv.inputs.nixpkgs.follows = "nixpkgs"; 7 | }; 8 | 9 | nixConfig = { 10 | extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="; 11 | extra-substituters = "https://devenv.cachix.org"; 12 | }; 13 | 14 | outputs = { self, nixpkgs, devenv, systems, ... } @ inputs: 15 | let 16 | forEachSystem = nixpkgs.lib.genAttrs (import systems); 17 | in 18 | { 19 | packages = forEachSystem (system: { 20 | devenv-up = self.devShells.${system}.default.config.procfileScript; 21 | }); 22 | 23 | devShells = forEachSystem 24 | (system: 25 | let 26 | pkgs = nixpkgs.legacyPackages.${system}; 27 | in 28 | { 29 | default = devenv.lib.mkShell { 30 | inherit inputs pkgs; 31 | modules = [ 32 | { 33 | packages = [ 34 | pkgs.git 35 | ]; 36 | 37 | languages.javascript = { 38 | enable = true; 39 | package = pkgs.nodejs_20; 40 | corepack.enable = true; 41 | }; 42 | 43 | services.mysql = { 44 | enable = true; 45 | package = pkgs.mysql80; 46 | settings = { 47 | mysqld = { 48 | interactive_timeout = 28800; 49 | port = 6033; 50 | wait_timeout = 28800; 51 | }; 52 | }; 53 | }; 54 | 55 | services.postgres = { 56 | enable = true; 57 | package = pkgs.postgresql_16; 58 | settings = { 59 | max_connections = 300; 60 | shared_buffers = "4GB"; 61 | }; 62 | listen_addresses = "localhost"; 63 | port = 2345; 64 | initialScript = '' 65 | CREATE USER postgres SUPERUSER; 66 | ''; 67 | }; 68 | } 69 | ]; 70 | }; 71 | }); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /knip.ts: -------------------------------------------------------------------------------- 1 | import { type KnipConfig } from "knip"; 2 | import { readFileSync } from "node:fs"; 3 | 4 | const optionalPeerDeps = Object.keys( 5 | ( 6 | JSON.parse(readFileSync("./packages/seed/package.json", "utf8")) as { 7 | peerDependenciesMeta: Record; 8 | } 9 | ).peerDependenciesMeta, 10 | ); 11 | 12 | const config: KnipConfig = { 13 | ignoreBinaries: ["nix"], 14 | ignoreDependencies: optionalPeerDeps.filter( 15 | (dep) => dep !== "@prisma/client", 16 | ), 17 | ignore: [ 18 | "./packages/seed/src/index.ts", 19 | "knip.ts", 20 | "./packages/seed/e2e/fixtures/install/**/*", 21 | "docs/**/*", 22 | ], 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@snaplet/root", 3 | "private": true, 4 | "type": "module", 5 | "workspaces": [ 6 | "packages/*", 7 | "docs" 8 | ], 9 | "devDependencies": { 10 | "@snaplet/eslint-config": "workspace:*", 11 | "@snaplet/tsconfig": "workspace:*", 12 | "eslint": "8.57.0", 13 | "knip": "5.11.0", 14 | "skott": "0.33.2", 15 | "turbo": "1.13.3", 16 | "typescript": "5.4.5", 17 | "vitest": "1.5.2" 18 | }, 19 | "scripts": { 20 | "build": "turbo run build", 21 | "clean": "turbo run clean", 22 | "clean-monorepo": "git clean -dfX -e \\!.env.local", 23 | "dev": "turbo run dev", 24 | "lint": "eslint --cache . --max-warnings 0 && turbo run lint", 25 | "lint-monorepo": "knip && knip --production --strict --include=unlisted", 26 | "test": "turbo run test", 27 | "test:e2e": "turbo run test:e2e", 28 | "type-check": "turbo run type-check", 29 | "type-check:watch": "tsc --build --watch", 30 | "update-deps": "pnpm update --interactive --recursive --latest", 31 | "update-infra": "nix flake update", 32 | "visualize-deps": "skott --displayMode=webapp --trackBuiltinDependencies --trackThirdPartyDependencies" 33 | }, 34 | "packageManager": "pnpm@8.15.5", 35 | "engines": { 36 | "node": ">=20.9.0" 37 | } 38 | } -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@snaplet/eslint-config", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "exports": { 6 | ".": { 7 | "types": "./src/index.d.ts", 8 | "default": "./src/index.js" 9 | } 10 | }, 11 | "dependencies": { 12 | "@typescript-eslint/eslint-plugin": "7.7.1", 13 | "@typescript-eslint/parser": "7.8.0", 14 | "eslint": "8.57.0", 15 | "eslint-config-prettier": "9.1.0", 16 | "eslint-plugin-i": "2.29.1", 17 | "eslint-plugin-node-import": "1.0.4", 18 | "eslint-plugin-perfectionist": "2.10.0", 19 | "eslint-plugin-prettier": "5.1.3", 20 | "eslint-plugin-unused-imports": "3.1.0", 21 | "eslint-plugin-vitest": "0.5.4", 22 | "globals": "15.1.0", 23 | "prettier": "3.2.5" 24 | }, 25 | "devDependencies": { 26 | "@types/eslint": "8.56.10" 27 | } 28 | } -------------------------------------------------------------------------------- /packages/eslint-config/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { type Linter } from "eslint"; 2 | 3 | declare function defineConfig>(config: T): T; 4 | 5 | declare const recommended: Array; 6 | declare const react: Array; 7 | 8 | export { 9 | defineConfig, 10 | recommended, 11 | react, 12 | }; -------------------------------------------------------------------------------- /packages/eslint-config/src/index.js: -------------------------------------------------------------------------------- 1 | export function defineConfig(config) { 2 | return config; 3 | } 4 | 5 | export * from "./recommended.js"; -------------------------------------------------------------------------------- /packages/seed/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true -------------------------------------------------------------------------------- /packages/seed/assets/index.ts: -------------------------------------------------------------------------------- 1 | const createSeedClient = () => { 2 | throw new Error( 3 | "@snaplet/seed client is missing. Please use npx @snaplet/seed sync or npx @snaplet/seed init to generate the client.", 4 | ); 5 | }; 6 | export { createSeedClient }; 7 | -------------------------------------------------------------------------------- /packages/seed/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "#cli/index.js"; 3 | -------------------------------------------------------------------------------- /packages/seed/e2e/api/e2e.api.reset.sqlite.test.ts: -------------------------------------------------------------------------------- 1 | import { test as _test, type TestFunction } from "vitest"; 2 | import { type DialectId } from "#dialects/dialects.js"; 3 | import { adapterEntries } from "#test/adapters.js"; 4 | import { setupProject } from "#test/setupProject.js"; 5 | 6 | type DialectRecordWithDefault = Partial> & 7 | Record<"default", T>; 8 | type SchemaRecord = DialectRecordWithDefault; 9 | 10 | for (const [dialect, adapter] of adapterEntries.filter( 11 | ([d]) => d === "sqlite", 12 | )) { 13 | const computeName = (name: string) => `e2e > api > ${dialect} > ${name}`; 14 | const test = (name: string, fn: TestFunction) => { 15 | // eslint-disable-next-line vitest/expect-expect, vitest/valid-title 16 | _test.concurrent(computeName(name), fn); 17 | }; 18 | 19 | test("seed.$resetDatabase should work on database withtout any sequence", async () => { 20 | const schema: SchemaRecord = { 21 | default: ` 22 | CREATE TABLE \`transform_null_rows\` ( 23 | \`id\` integer PRIMARY KEY NOT NULL, 24 | \`dates_id\` integer NOT NULL, 25 | \`users_id\` integer NOT NULL, 26 | \`merge_requests_id\` integer NOT NULL, 27 | \`repository_id\` integer NOT NULL, 28 | \`__created_at\` integer DEFAULT (strftime('%s', 'now')), 29 | \`__updated_at\` integer DEFAULT (strftime('%s', 'now')) 30 | ); 31 | `, 32 | }; 33 | const seedScript = ` 34 | import { createSeedClient } from '#snaplet/seed' 35 | const seed = await createSeedClient() 36 | await seed.$resetDatabase() 37 | await seed.transformNullRows((x) => x(10)); 38 | `; 39 | const { runSeedScript } = await setupProject({ 40 | adapter, 41 | databaseSchema: schema[dialect] ?? schema.default, 42 | seedScript, 43 | }); 44 | // Should be able to re-run the seed script again thanks to the $resetDatabase 45 | await runSeedScript(seedScript); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /packages/seed/e2e/fixtures/install/.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true -------------------------------------------------------------------------------- /packages/seed/e2e/fixtures/install/.snaplet/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "adapter": "postgres" 3 | } -------------------------------------------------------------------------------- /packages/seed/e2e/fixtures/install/.snaplet/dataModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "models": { 3 | "test_table": { 4 | "id": "public.test_table", 5 | "schemaName": "public", 6 | "tableName": "test_table", 7 | "fields": [ 8 | { 9 | "id": "public.test_table.name", 10 | "name": "name", 11 | "columnName": "name", 12 | "type": "char", 13 | "isRequired": false, 14 | "kind": "scalar", 15 | "isList": false, 16 | "isGenerated": false, 17 | "sequence": false, 18 | "hasDefaultValue": false, 19 | "isId": false, 20 | "maxLength": null 21 | } 22 | ], 23 | "uniqueConstraints": [] 24 | } 25 | }, 26 | "enums": {} 27 | } -------------------------------------------------------------------------------- /packages/seed/e2e/fixtures/install/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-repo", 3 | "private": true, 4 | "type": "module", 5 | "devDependencies": { 6 | "@snaplet/copycat": "^5.0.0", 7 | "@snaplet/seed": "file:snaplet-seed.tgz", 8 | "postgres": "3.4.4" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/seed/e2e/fixtures/install/seed.config.ts: -------------------------------------------------------------------------------- 1 | import { SeedPostgres } from "@snaplet/seed/adapter-postgres"; 2 | import { defineConfig } from "@snaplet/seed/config"; 3 | import postgres from "postgres"; 4 | 5 | export default defineConfig({ 6 | adapter: () => { 7 | const client = postgres(""); 8 | return new SeedPostgres(client); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/seed/e2e/fixtures/install/seed.ts: -------------------------------------------------------------------------------- 1 | import { createSeedClient } from "@snaplet/seed"; 2 | 3 | const main = async () => { 4 | const seed = await createSeedClient({}); 5 | 6 | await seed.$resetDatabase(); 7 | 8 | process.exit(); 9 | }; 10 | 11 | main(); -------------------------------------------------------------------------------- /packages/seed/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, recommended } from "@snaplet/eslint-config"; 2 | 3 | export default defineConfig([ 4 | ...recommended, 5 | { 6 | ignores: ["e2e/fixtures/install/**"], 7 | }, 8 | ]); 9 | -------------------------------------------------------------------------------- /packages/seed/global.d.ts: -------------------------------------------------------------------------------- 1 | import "@total-typescript/ts-reset"; 2 | 3 | module "exit-hook" { 4 | export function gracefulExit(signal?: number): never; 5 | } 6 | -------------------------------------------------------------------------------- /packages/seed/scripts/patch-defineConfig.mts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "node:fs/promises"; 2 | import { EOL } from "node:os"; 3 | import { join } from "node:path"; 4 | 5 | /** 6 | * This script patches the defineConfig.d.ts file to include the reference to the assets/defineConfig.d.ts type 7 | * so the user doesn't have to import it manually in their seed.config.ts file to have the TypedConfig type available. 8 | */ 9 | 10 | const defineConfigPath = join( 11 | ".", 12 | "dist", 13 | "src", 14 | "config", 15 | "seedConfig", 16 | "defineConfig.d.ts", 17 | ); 18 | 19 | await writeFile( 20 | defineConfigPath, 21 | [ 22 | "// @ts-ignore", 23 | `/// `, 24 | await readFile(defineConfigPath, "utf-8"), 25 | ].join(EOL), 26 | ); 27 | -------------------------------------------------------------------------------- /packages/seed/scripts/publishToDiscord.mts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import { z } from "zod"; 3 | import { getGitHubRelease } from "./release-utils.mjs"; 4 | 5 | const env = z 6 | .object({ 7 | GITHUB_TOKEN: z.string(), 8 | RELEASE_TAG: z.string(), 9 | DISCORD_WEBHOOK_URL: z.string(), 10 | }) 11 | .parse(process.env); 12 | 13 | const release = await getGitHubRelease({ 14 | githubToken: env.GITHUB_TOKEN, 15 | releaseTag: env.RELEASE_TAG, 16 | }); 17 | 18 | // patch code blocks because Discord support is not great 19 | let content = release.body 20 | .replace(/```(\w+).+$/gm, "```$1") 21 | .replace(/\s+```/gm, "\n```"); 22 | 23 | if (content.length > 2000) { 24 | // example: #v0970---30-april-2024 25 | const anchor = 26 | `#${release.tagName.replaceAll(".", "")}---${dayjs(release.publishedAt).format("DD-MMMM-YYYY")}`.toLowerCase(); 27 | const docsLink = `https://docs.snaplet.dev/seed/release-notes${anchor}`; 28 | const readMore = `[Read more](${docsLink})`; 29 | const extraContent = `...\n\n${readMore}`; 30 | content = [content.slice(0, 2000 - extraContent.length), extraContent].join( 31 | "", 32 | ); 33 | } 34 | 35 | await fetch(env.DISCORD_WEBHOOK_URL, { 36 | method: "POST", 37 | headers: { 38 | "Content-Type": "application/json", 39 | }, 40 | body: JSON.stringify({ 41 | content, 42 | }), 43 | }); 44 | -------------------------------------------------------------------------------- /packages/seed/scripts/publishToDocs.mts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "@octokit/rest"; 2 | import dayjs from "dayjs"; 3 | import { z } from "zod"; 4 | import { getGitHubRelease } from "./release-utils.mjs"; 5 | 6 | const env = z 7 | .object({ 8 | GITHUB_TOKEN: z.string(), 9 | RELEASE_TAG: z.string(), 10 | }) 11 | .parse(process.env); 12 | 13 | const octokit = new Octokit({ auth: env.GITHUB_TOKEN }); 14 | 15 | const release = await getGitHubRelease({ 16 | githubToken: env.GITHUB_TOKEN, 17 | releaseTag: env.RELEASE_TAG, 18 | }); 19 | 20 | const filename = `${dayjs().format("YYYYMMDDHHmmss")}-${env.RELEASE_TAG}.md`; 21 | 22 | await octokit.repos.createOrUpdateFileContents({ 23 | owner: "snaplet", 24 | repo: "docs", 25 | path: `releases/${filename}`, 26 | message: `Release notes for ${env.RELEASE_TAG}`, 27 | content: Buffer.from(release.body).toString("base64"), 28 | branch: "main", 29 | }); 30 | -------------------------------------------------------------------------------- /packages/seed/scripts/release-alpha.mts: -------------------------------------------------------------------------------- 1 | import semver from "semver"; 2 | import { readPkg, writePkg } from "../src/core/version.js"; 3 | import { releaseSnapletSeed } from "./release-utils.mjs"; 4 | 5 | const seedPackage = readPkg<{ version: string }>(); 6 | 7 | // We bump the alpha to the next minor version 8 | const nextMinorVersion = semver.inc(seedPackage.version, "minor"); 9 | // We append "alpha" to the current version so it doesn't take over the stable version by semver rules 10 | const alphaVersion = `${nextMinorVersion}-alpha`; 11 | // We append the git hash to the alpha version if it is available so we can deploy multiples alpha versions on the same stable version 12 | const gitAlphaVersion = process.env["GIT_HASH"] 13 | ? `${alphaVersion}-${process.env["GIT_HASH"]}` 14 | : alphaVersion; 15 | 16 | // At this point our version will look like -alpha- 17 | seedPackage.version = gitAlphaVersion; 18 | // Override the version in the package.json file with the new alpha version 19 | // So it's the one that will be used in the build 20 | writePkg(seedPackage); 21 | const versionToRelease = gitAlphaVersion; 22 | const channel = "alpha"; 23 | 24 | try { 25 | releaseSnapletSeed({ versionToRelease, channel, dryRun: false }); 26 | process.exit(0); 27 | } catch (error) { 28 | process.exit(1); 29 | } 30 | -------------------------------------------------------------------------------- /packages/seed/scripts/release-latest.mts: -------------------------------------------------------------------------------- 1 | import { readPkg } from "../src/core/version.js"; 2 | import { releaseSnapletSeed } from "./release-utils.mjs"; 3 | 4 | const seedPackage = readPkg<{ version: string }>(); 5 | const versionToRelease = seedPackage.version; 6 | const channel = "latest"; 7 | 8 | try { 9 | releaseSnapletSeed({ versionToRelease, channel, dryRun: false }); 10 | process.exit(0); 11 | } catch (error) { 12 | process.exit(1); 13 | } 14 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/better-sqlite3/better-sqlite3.ts: -------------------------------------------------------------------------------- 1 | import { type Database } from "better-sqlite3"; 2 | import dedent from "dedent"; 3 | import { DatabaseClient } from "#core/databaseClient.js"; 4 | import { type Adapter } from "../types.js"; 5 | 6 | export class SeedBetterSqlite3 extends DatabaseClient { 7 | static id = "better-sqlite3" as const; 8 | 9 | // eslint-disable-next-line @typescript-eslint/require-await 10 | async execute(query: string): Promise { 11 | this.client.prepare(query).run(); 12 | } 13 | // eslint-disable-next-line @typescript-eslint/require-await 14 | async query(query: string): Promise> { 15 | const res = this.client.prepare(query).all(); 16 | return res as Array; 17 | } 18 | } 19 | 20 | export const betterSqlite3Adapter = { 21 | getDialect: () => "sqlite", 22 | id: "better-sqlite3" as const, 23 | name: "better-sqlite3", 24 | packageName: "better-sqlite3", 25 | typesPackageName: "@types/better-sqlite3", 26 | template: ( 27 | parameters = `'', { fileMustExist: true }`, 28 | ) => dedent` 29 | import { SeedBetterSqlite3 } from "@snaplet/seed/adapter-better-sqlite3"; 30 | import { defineConfig } from "@snaplet/seed/config"; 31 | import Database from "better-sqlite3"; 32 | 33 | export default defineConfig({ 34 | adapter: () => { 35 | const client = new Database(${parameters}); 36 | return new SeedBetterSqlite3(client); 37 | }, 38 | }); 39 | `, 40 | } satisfies Adapter; 41 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/better-sqlite3/index.ts: -------------------------------------------------------------------------------- 1 | export type { DatabaseClient } from "#core/databaseClient.js"; 2 | 3 | export { SeedBetterSqlite3 } from "./better-sqlite3.js"; 4 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/getAdapter.ts: -------------------------------------------------------------------------------- 1 | import { getProjectConfig } from "#config/project/projectConfig.js"; 2 | import { type AdapterId, adapters } from "./index.js"; 3 | import { type Adapter } from "./types.js"; 4 | 5 | export async function getAdapter(id?: AdapterId): Promise { 6 | if (id) { 7 | return adapters[id]; 8 | } 9 | 10 | const projectConfig = await getProjectConfig(); 11 | if (projectConfig.adapter === undefined) { 12 | throw new Error( 13 | "Adapter not found, please ensure that the 'adapter' key is set in `.snaplet/config.json`", 14 | ); 15 | } 16 | 17 | return adapters[projectConfig.adapter]; 18 | } 19 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/getDatabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { getSeedConfig } from "#config/seedConfig/seedConfig.js"; 2 | import { type DatabaseClient } from "#core/databaseClient.js"; 3 | import { SnapletError } from "#core/utils.js"; 4 | 5 | let databaseClient: DatabaseClient | undefined; 6 | 7 | export async function getDatabaseClient() { 8 | if (databaseClient) { 9 | return databaseClient; 10 | } 11 | 12 | // patching the seedConfig requires the dataModel which doesn't exist yet during the introspection 13 | // this will be better when we will split the config between adapter.ts and seed.config.json 14 | const seedConfig = await getSeedConfig({ 15 | disablePatch: true, 16 | }); 17 | 18 | try { 19 | const _databaseClient = await seedConfig.adapter(); 20 | await _databaseClient.query("SELECT 1"); 21 | databaseClient = _databaseClient; 22 | } catch (error) { 23 | throw new SnapletError("SEED_ADAPTER_CANNOT_CONNECT", { 24 | error: error as Error, 25 | }); 26 | } 27 | 28 | return databaseClient; 29 | } 30 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | import { betterSqlite3Adapter } from "./better-sqlite3/better-sqlite3.js"; 2 | import { mysql2Adapter } from "./mysql2/mysql2.js"; 3 | import { pgAdapter } from "./pg/pg.js"; 4 | import { postgresAdapter } from "./postgres/postgres.js"; 5 | import { prismaAdapter } from "./prisma/prisma.js"; 6 | 7 | export const ormAdapters = { 8 | [prismaAdapter.id]: prismaAdapter, 9 | }; 10 | 11 | export const postgresAdapters = { 12 | [pgAdapter.id]: pgAdapter, 13 | [postgresAdapter.id]: postgresAdapter, 14 | }; 15 | 16 | export const sqliteAdapters = { 17 | [betterSqlite3Adapter.id]: betterSqlite3Adapter, 18 | }; 19 | 20 | export const mysqlAdapters = { 21 | [mysql2Adapter.id]: mysql2Adapter, 22 | }; 23 | 24 | export const adapters = { 25 | ...ormAdapters, 26 | ...postgresAdapters, 27 | ...sqliteAdapters, 28 | ...mysqlAdapters, 29 | }; 30 | 31 | export type AdapterId = keyof typeof adapters; 32 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/mysql2/index.ts: -------------------------------------------------------------------------------- 1 | export type { DatabaseClient } from "#core/databaseClient.js"; 2 | 3 | export { SeedMysql2 } from "./mysql2.js"; 4 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/mysql2/mysql2.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { type Connection, type Pool } from "mysql2/promise"; 3 | import { DatabaseClient } from "#core/databaseClient.js"; 4 | import { type Adapter } from "../types.js"; 5 | 6 | export class SeedMysql2 extends DatabaseClient { 7 | async execute(query: string): Promise { 8 | await this.client.query(query); 9 | } 10 | 11 | async query(query: string): Promise> { 12 | const [results] = await this.client.query(query); 13 | return results as Array; 14 | } 15 | } 16 | 17 | export const mysql2Adapter = { 18 | getDialect: () => "mysql", 19 | id: "mysql2" as const, 20 | name: "mysql2", 21 | packageName: "mysql2", 22 | template: (parameters = `/* connection string */`) => dedent` 23 | import { SeedMysql2 } from "@snaplet/seed/adapter-mysql2"; 24 | import { defineConfig } from "@snaplet/seed/config"; 25 | import { createConnection } from "mysql2/promise"; 26 | 27 | export default defineConfig({ 28 | adapter: async () => { 29 | const client = await createConnection(${parameters}); 30 | await client.connect(); 31 | return new SeedMysql2(client); 32 | }, 33 | }); 34 | `, 35 | } satisfies Adapter; 36 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/pg/index.ts: -------------------------------------------------------------------------------- 1 | export type { DatabaseClient } from "#core/databaseClient.js"; 2 | 3 | export { SeedPg } from "./pg.js"; 4 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/pg/pg.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { type Client } from "pg"; 3 | import { DatabaseClient } from "#core/databaseClient.js"; 4 | import { type Adapter } from "../types.js"; 5 | 6 | export class SeedPg extends DatabaseClient { 7 | async execute(query: string): Promise { 8 | await this.client.query(query); 9 | } 10 | 11 | async query(query: string): Promise> { 12 | const { rows } = await this.client.query(query); 13 | return rows as Array; 14 | } 15 | } 16 | 17 | export const pgAdapter = { 18 | getDialect: () => "postgres", 19 | id: "pg" as const, 20 | name: "node-postgres", 21 | packageName: "pg", 22 | typesPackageName: "@types/pg", 23 | template: (parameters = `/* connection parameters */`) => dedent` 24 | import { SeedPg } from "@snaplet/seed/adapter-pg"; 25 | import { defineConfig } from "@snaplet/seed/config"; 26 | import { Client } from "pg"; 27 | 28 | export default defineConfig({ 29 | adapter: async () => { 30 | const client = new Client(${parameters}); 31 | await client.connect(); 32 | return new SeedPg(client); 33 | }, 34 | }); 35 | `, 36 | } satisfies Adapter; 37 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/postgres/index.ts: -------------------------------------------------------------------------------- 1 | export type { DatabaseClient } from "#core/databaseClient.js"; 2 | 3 | export { SeedPostgres } from "./postgres.js"; 4 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/postgres/postgres.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { type Sql } from "postgres"; 3 | import { DatabaseClient } from "#core/databaseClient.js"; 4 | import { type Adapter } from "../types.js"; 5 | 6 | export class SeedPostgres extends DatabaseClient { 7 | async execute(query: string): Promise { 8 | await this.client.unsafe(query); 9 | } 10 | 11 | async query(query: string): Promise> { 12 | const res = await this.client.unsafe(query); 13 | return res as unknown as Array; 14 | } 15 | } 16 | 17 | export const postgresAdapter = { 18 | getDialect: () => "postgres", 19 | id: "postgres" as const, 20 | name: "Postgres.js", 21 | packageName: "postgres", 22 | template: (parameters = `/* connection parameters */`) => dedent` 23 | import { SeedPostgres } from "@snaplet/seed/adapter-postgres"; 24 | import { defineConfig } from "@snaplet/seed/config"; 25 | import postgres from "postgres"; 26 | 27 | export default defineConfig({ 28 | adapter: () => { 29 | const client = postgres(${parameters}); 30 | return new SeedPostgres(client); 31 | }, 32 | }); 33 | `, 34 | } satisfies Adapter; 35 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/prisma/getDialect.ts: -------------------------------------------------------------------------------- 1 | export async function getDialect() { 2 | const prismaInternals = await import("@prisma/internals"); 3 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 4 | const { getConfig, getSchema } = prismaInternals.default ?? prismaInternals; 5 | 6 | const config = await getConfig({ 7 | datamodel: await getSchema(), 8 | ignoreEnvVarErrors: true, 9 | }); 10 | 11 | const provider = config.datasources.at(0)?.provider; 12 | 13 | switch (provider) { 14 | case "mysql": 15 | return "mysql"; 16 | case "postgres": 17 | case "postgresql": 18 | return "postgres"; 19 | case "sqlite": 20 | return "sqlite"; 21 | default: 22 | throw new Error(`Unsupported Prisma provider ${provider}`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/prisma/getPrismaDataModel.ts: -------------------------------------------------------------------------------- 1 | import { type DMMF } from "@prisma/generator-helper"; 2 | 3 | export async function getPrismaDataModel(): Promise { 4 | const prismaInternals = await import("@prisma/internals"); 5 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 6 | const { getDMMF, getSchema } = prismaInternals.default ?? prismaInternals; 7 | 8 | const datamodel = await getSchema(); 9 | 10 | return getDMMF({ 11 | datamodel, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/prisma/index.ts: -------------------------------------------------------------------------------- 1 | export type { DatabaseClient } from "#core/databaseClient.js"; 2 | 3 | export { SeedPrisma } from "./prisma.js"; 4 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/prisma/prisma.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { DatabaseClient } from "#core/databaseClient.js"; 3 | import { type Adapter } from "../types.js"; 4 | import { getDialect } from "./getDialect.js"; 5 | import { patchSeedConfig } from "./patchSeedConfig.js"; 6 | import { patchUserModels } from "./patchUserModels.js"; 7 | 8 | interface PrismaLikeClient { 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | $executeRawUnsafe(query: string, ...values: Array): Promise; 11 | $queryRawUnsafe( 12 | query: string, 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | ...values: Array 15 | ): Promise; 16 | // https://github.com/prisma/prisma/blob/ed2f2fc22c5847839b8fd742192fa3b4ad5e78d6/packages/client/src/runtime/getPrismaClient.ts#L404 17 | _engineConfig?: { 18 | // https://github.com/prisma/prisma/blob/ed2f2fc22c5847839b8fd742192fa3b4ad5e78d6/packages/client/tests/functional/_utils/providers.ts#L1-L8 19 | activeProvider: 20 | | "cockroachdb" 21 | | "mongodb" 22 | | "mysql" 23 | | "postgresql" 24 | | "sqlite" 25 | | "sqlserver"; 26 | }; 27 | } 28 | 29 | export class SeedPrisma extends DatabaseClient { 30 | async execute(query: string): Promise { 31 | await this.client.$executeRawUnsafe(query); 32 | } 33 | 34 | async query(query: string): Promise> { 35 | const res = await this.client.$queryRawUnsafe(query); 36 | return res as Array; 37 | } 38 | } 39 | 40 | export const prismaAdapter = { 41 | getDialect, 42 | id: "prisma" as const, 43 | name: "Prisma", 44 | packageName: "@prisma/client", 45 | template: (parameters = ``) => dedent` 46 | import { SeedPrisma } from "@snaplet/seed/adapter-prisma"; 47 | import { defineConfig } from "@snaplet/seed/config"; 48 | import { PrismaClient } from "@prisma/client"; 49 | 50 | export default defineConfig({ 51 | adapter: () => { 52 | const client = new PrismaClient(${parameters}); 53 | return new SeedPrisma(client); 54 | }, 55 | select: ["!*_prisma_migrations"], 56 | }); 57 | `, 58 | patchSeedConfig, 59 | patchUserModels, 60 | } satisfies Adapter; 61 | -------------------------------------------------------------------------------- /packages/seed/src/adapters/types.ts: -------------------------------------------------------------------------------- 1 | import { type SeedConfig } from "#config/seedConfig/seedConfig.js"; 2 | import { type DataModel } from "#core/dataModel/types.js"; 3 | import { type UserModels } from "#core/userModels/types.js"; 4 | import { type DialectId } from "#dialects/dialects.js"; 5 | 6 | export interface Adapter { 7 | getDialect: () => DialectId | Promise; 8 | id: string; 9 | name: string; 10 | packageName: string; 11 | patchSeedConfig?: (props: { 12 | dataModel: DataModel; 13 | seedConfig: SeedConfig; 14 | }) => Promise; 15 | patchUserModels?: (props: { 16 | dataModel: DataModel; 17 | dialect: DialectId; 18 | userModels: UserModels; 19 | }) => Promise; 20 | template: (parameters?: string) => string; 21 | typesPackageName?: string; 22 | } 23 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/config.ts: -------------------------------------------------------------------------------- 1 | import { type Argv } from "yargs"; 2 | 3 | export function configOption(program: Argv) { 4 | program 5 | .option("config", { 6 | type: "string", 7 | description: "Path to the config file", 8 | }) 9 | .middleware((argv) => { 10 | if (argv.config) { 11 | process.env["SNAPLET_SEED_CONFIG"] = argv.config; 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/generate/computeCodegenContext.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getSeedConfig, 3 | getSeedConfigPath, 4 | } from "#config/seedConfig/seedConfig.js"; 5 | import { type CodegenContext } from "#core/codegen/codegen.js"; 6 | import { getDataModel, getRawDataModel } from "#core/dataModel/dataModel.js"; 7 | import { getFingerprint } from "#core/fingerprint/fingerprint.js"; 8 | import { getDataExamples } from "#core/predictions/shapeExamples/getDataExamples.js"; 9 | import { getShapePredictions } from "#core/predictions/shapePredictions/getShapePredictions.js"; 10 | import { getDialect } from "#dialects/getDialect.js"; 11 | 12 | export async function computeCodegenContext(props: { 13 | outputDir: string | undefined; 14 | }): Promise { 15 | const { outputDir } = props; 16 | 17 | const rawDataModel = await getRawDataModel(); 18 | const dataModel = await getDataModel(); 19 | const seedConfig = await getSeedConfig(); 20 | const seedConfigPath = await getSeedConfigPath(); 21 | const dialect = await getDialect(); 22 | 23 | return { 24 | seedConfig, 25 | seedConfigPath, 26 | fingerprint: await getFingerprint(), 27 | dataModel, 28 | rawDataModel, 29 | outputDir, 30 | shapePredictions: await getShapePredictions(), 31 | dataExamples: await getDataExamples(), 32 | dialect, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/generate/generate.ts: -------------------------------------------------------------------------------- 1 | import { type Argv } from "yargs"; 2 | 3 | export function generateCommand(program: Argv) { 4 | return program.command( 5 | "generate", 6 | "Generate artifacts (e.g. Seed Client)", 7 | { 8 | output: { 9 | hidden: true, 10 | alias: "o", 11 | describe: "A custom directory path to output the generated assets to", 12 | type: "string", 13 | }, 14 | }, 15 | async (args) => { 16 | const { generateHandler } = await import("./generateHandler.js"); 17 | await generateHandler(args); 18 | }, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/generate/generateHandler.ts: -------------------------------------------------------------------------------- 1 | import { generateAssets } from "#core/codegen/codegen.js"; 2 | import { computeCodegenContext } from "./computeCodegenContext.js"; 3 | 4 | export async function generateHandler(args: { output?: string }) { 5 | try { 6 | const context = await computeCodegenContext({ outputDir: args.output }); 7 | 8 | await generateAssets(context); 9 | 10 | return { ok: true }; 11 | } catch (error) { 12 | throw error as Error; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/adapterHandler.ts: -------------------------------------------------------------------------------- 1 | import { adapters } from "#adapters/index.js"; 2 | import { updateProjectConfig } from "#config/project/projectConfig.js"; 3 | import { getInstalledDependencies } from "#config/utils.js"; 4 | import { selectAdapterFromPrompt } from "./selectAdapterFromPrompt.js"; 5 | 6 | export async function adapterHandler() { 7 | const adapter = await selectAdapter(); 8 | await updateProjectConfig({ adapter: adapter.id }); 9 | return adapter; 10 | } 11 | 12 | async function selectAdapter() { 13 | const installedDependencies = await getInstalledDependencies(); 14 | 15 | // look for ORM-like adapters first 16 | if (installedDependencies["@prisma/client"]) { 17 | return adapters.prisma; 18 | } 19 | 20 | // look for library adapters next 21 | const libraryAdapters = Object.values(adapters).filter((a) => 22 | Boolean(installedDependencies[a.packageName]), 23 | ); 24 | 25 | // no ambiguity, return the adapter 26 | if (libraryAdapters.length === 1) { 27 | return libraryAdapters[0]; 28 | } 29 | 30 | // if more than one or no adapter is found, prompt the user 31 | return selectAdapterFromPrompt(); 32 | } 33 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/generateSeedScriptExample.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { existsSync } from "node:fs"; 3 | import { writeFile } from "node:fs/promises"; 4 | import { dirname, join } from "node:path"; 5 | import { getSeedConfigPath } from "#config/seedConfig/seedConfig.js"; 6 | import { getDataModel } from "#core/dataModel/dataModel.js"; 7 | import { sortModels } from "#core/store/topologicalSort.js"; 8 | 9 | export async function generateSeedScriptExample() { 10 | const seedScriptPath = join(dirname(await getSeedConfigPath()), "seed.ts"); 11 | 12 | // If the seed script already exists, we don't want to overwrite it 13 | if (existsSync(seedScriptPath)) { 14 | return seedScriptPath; 15 | } 16 | 17 | const dataModel = await getDataModel(); 18 | const [model] = sortModels(dataModel); 19 | const template = dedent` 20 | /** 21 | * ! Executing this script will delete all data in your database and seed it with 10 ${model.modelName}. 22 | * ! Make sure to adjust the script to your needs. 23 | * Use any TypeScript runner to run this script, for example: \`npx tsx seed.ts\` 24 | * Learn more about the Seed Client by following our guide: https://docs.snaplet.dev/seed/getting-started 25 | */ 26 | import { createSeedClient } from "@snaplet/seed"; 27 | 28 | const main = async () => { 29 | const seed = await createSeedClient(); 30 | 31 | // Truncate all tables in the database 32 | await seed.$resetDatabase(); 33 | 34 | // Seed the database with 10 ${model.modelName} 35 | await seed.${model.modelName}((x) => x(10)); 36 | 37 | // Type completion not working? You might want to reload your TypeScript Server to pick up the changes 38 | 39 | console.log("Database seeded successfully!"); 40 | 41 | process.exit(); 42 | }; 43 | 44 | main(); 45 | `; 46 | 47 | await writeFile(seedScriptPath, template); 48 | 49 | return seedScriptPath; 50 | } 51 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/init.ts: -------------------------------------------------------------------------------- 1 | import { type Argv } from "yargs"; 2 | 3 | export function initCommand(program: Argv) { 4 | return program.command( 5 | "init [directory]", 6 | "Initialize Snaplet Seed locally for your project", 7 | (y) => 8 | y.positional("directory", { 9 | type: "string", 10 | describe: "Directory path to initialize Snaplet Seed in", 11 | default: ".", 12 | }), 13 | async (args) => { 14 | const { initHandler } = await import("./initHandler.js"); 15 | await initHandler(args); 16 | }, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/installDependencies.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from "node:path"; 2 | import { type Adapter } from "#adapters/types.js"; 3 | import { 4 | getInstalledDependencies, 5 | getPackageJsonPath, 6 | getPackageManager, 7 | } from "#config/utils.js"; 8 | import { getVersion } from "#core/version.js"; 9 | import { spinner } from "../../lib/output.js"; 10 | 11 | export async function installDependencies({ adapter }: { adapter: Adapter }) { 12 | const installedDependencies = await getInstalledDependencies(); 13 | 14 | const devDependenciesToInstall = [ 15 | "@snaplet/copycat", 16 | `@snaplet/seed@${getVersion()}`, 17 | adapter.packageName, 18 | ...(adapter.typesPackageName ? [adapter.typesPackageName] : []), 19 | ].filter((d) => { 20 | if (d.startsWith("@snaplet/seed")) { 21 | return !installedDependencies["@snaplet/seed"]; 22 | } 23 | return !installedDependencies[d]; 24 | }); 25 | 26 | if (devDependenciesToInstall.length === 0) { 27 | return; 28 | } 29 | 30 | const devDependenciesToInstallList = devDependenciesToInstall 31 | .sort((a, b) => a.localeCompare(b)) 32 | .map((d) => `\`${d}\``) 33 | .join(", "); 34 | 35 | console.log(); 36 | spinner.start(`Installing the dependencies: ${devDependenciesToInstallList}`); 37 | 38 | const packageManager = await getPackageManager(); 39 | const packageJsonPath = await getPackageJsonPath(); 40 | 41 | await packageManager.add(devDependenciesToInstall, { 42 | dev: true, 43 | cwd: dirname(packageJsonPath), 44 | }); 45 | 46 | spinner.succeed( 47 | `Installed the dependencies: ${devDependenciesToInstallList}`, 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/isConnected.ts: -------------------------------------------------------------------------------- 1 | import { getDatabaseClient } from "#adapters/getDatabaseClient.js"; 2 | 3 | export async function isConnected() { 4 | try { 5 | await getDatabaseClient(); 6 | return { result: true }; 7 | } catch (err) { 8 | return { result: false, reason: err }; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/saveSeedConfig.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from "node:url"; 2 | import { type Adapter } from "#adapters/types.js"; 3 | import { 4 | getSeedConfigPath, 5 | setSeedConfig, 6 | } from "#config/seedConfig/seedConfig.js"; 7 | import { eraseLines, error, link, spinner } from "../../lib/output.js"; 8 | import { isConnected } from "./isConnected.js"; 9 | import { watchFile } from "./utils.js"; 10 | 11 | export async function saveSeedConfig({ adapter }: { adapter: Adapter }) { 12 | await setSeedConfig(adapter.template()); 13 | 14 | const seedConfigPath = await getSeedConfigPath(); 15 | 16 | console.log(); 17 | spinner.succeed( 18 | `Seed configuration saved to ${link(pathToFileURL(seedConfigPath).toString())}`, 19 | ); 20 | 21 | if ((await isConnected()).result) { 22 | return; 23 | } 24 | 25 | spinner.start( 26 | `Please enter your database connection details by editing the Seed configuration file`, 27 | ); 28 | 29 | const watcher = watchFile(seedConfigPath); 30 | let attempt = 0; 31 | for await (const event of watcher) { 32 | if (event.eventType === "change") { 33 | attempt += 1; 34 | spinner.text = `Please enter your database connection details by editing the Seed configuration file (attempt: ${attempt})`; 35 | const connectionAttempt = await isConnected(); 36 | if (connectionAttempt.result) { 37 | spinner.stop(); 38 | eraseLines(1); 39 | break; 40 | } else { 41 | spinner.suffixText = `\n${error("Connection failed:")} ${connectionAttempt.reason}\n`; 42 | } 43 | } 44 | } 45 | spinner.suffixText = ``; 46 | spinner.text = ``; 47 | } 48 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/selectAdapterFromPrompt.ts: -------------------------------------------------------------------------------- 1 | import { Separator, select } from "@inquirer/prompts"; 2 | import { gracefulExit } from "exit-hook"; 3 | import { 4 | type AdapterId, 5 | adapters, 6 | mysqlAdapters, 7 | ormAdapters, 8 | postgresAdapters, 9 | sqliteAdapters, 10 | } from "#adapters/index.js"; 11 | import { type Adapter } from "#adapters/types.js"; 12 | import { dim } from "#cli/lib/output.js"; 13 | 14 | export async function selectAdapterFromPrompt() { 15 | const adapterId = await select({ 16 | message: "Select an adapter to connect Seed to your database:", 17 | choices: [ 18 | new Separator("ORM 🛠️"), 19 | ...formatAdapters(ormAdapters), 20 | new Separator("PostgreSQL 🐘"), 21 | ...formatAdapters(postgresAdapters), 22 | new Separator("SQLite 🪶"), 23 | ...formatAdapters(sqliteAdapters), 24 | new Separator("MySQL 🐬"), 25 | ...formatAdapters(mysqlAdapters), 26 | ], 27 | }).catch(() => { 28 | gracefulExit(); 29 | }); 30 | 31 | const adapter = adapters[adapterId]; 32 | 33 | return adapter; 34 | } 35 | 36 | function formatAdapters(adapters: Record) { 37 | return Object.values(adapters) 38 | .map(({ id, name, packageName }) => ({ 39 | value: id as AdapterId, 40 | name: `${name} ${dim(`(${packageName})`)}`, 41 | })) 42 | .sort((a, b) => a.name.localeCompare(b.name)); 43 | } 44 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/init/utils.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "node:crypto"; 2 | import { readFile, watch } from "node:fs/promises"; 3 | 4 | async function getFileHash(filePath: string) { 5 | const data = await readFile(filePath); 6 | return createHash("md5").update(data).digest("hex"); 7 | } 8 | 9 | const DEBOUNCE_MS = 200; 10 | // Because nodejs watch api is broken, we need this to actually watch for 11 | // file change and only trigger once for every actual change 12 | // See: https://github.com/nodejs/node-v0.x-archive/issues/2126 13 | export async function* watchFile(filePath: string) { 14 | let md5Previous: null | string = null; 15 | 16 | // We get the current hash of the file when calling the watch for first time 17 | // This avoid the watch to raise a change event when the file is first read 18 | try { 19 | md5Previous = await getFileHash(filePath); 20 | } catch (err) { 21 | // ignore error 22 | } 23 | 24 | let fsWait = false; 25 | const watcher = watch(filePath); 26 | 27 | for await (const { eventType, filename } of watcher) { 28 | if (!filename || eventType !== "change") continue; 29 | if (fsWait) continue; 30 | 31 | fsWait = true; 32 | setTimeout(() => { 33 | fsWait = false; 34 | }, DEBOUNCE_MS); 35 | 36 | try { 37 | const md5Current = await getFileHash(filePath); 38 | if (md5Current === md5Previous) { 39 | continue; 40 | } 41 | md5Previous = md5Current; 42 | yield { filename, eventType: "change", md5Hash: md5Current }; 43 | } catch (err) { 44 | continue; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/introspect/introspect.ts: -------------------------------------------------------------------------------- 1 | import { type Argv } from "yargs"; 2 | 3 | export function introspectCommand(program: Argv) { 4 | return program.command("introspect", false, {}, async () => { 5 | const { introspectHandler } = await import("./introspectHandler.js"); 6 | await introspectHandler(); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/introspect/introspectHandler.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { pathToFileURL } from "node:url"; 3 | import { getDatabaseClient } from "#adapters/getDatabaseClient.js"; 4 | import { 5 | getDataModelConfigPath, 6 | setDataModelConfig, 7 | } from "#config/dataModelConfig.js"; 8 | import { getDialect } from "#dialects/getDialect.js"; 9 | import { dim, link, spinner } from "../../lib/output.js"; 10 | 11 | export async function introspectHandler() { 12 | try { 13 | console.log(); 14 | spinner.start(`Analysing your database structure 🔍`); 15 | 16 | const dialect = await getDialect(); 17 | const databaseClient = await getDatabaseClient(); 18 | const dataModel = await dialect.getDataModel(databaseClient); 19 | 20 | if (Object.keys(dataModel.models).length === 0) { 21 | throw new Error( 22 | "No tables found in the database, please make sure the database is not empty", 23 | ); 24 | } 25 | 26 | await setDataModelConfig(dataModel); 27 | 28 | // we know the path exists because we just called `setDataModelConfig` 29 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 30 | const dataModelConfigPath = (await getDataModelConfigPath())!; 31 | spinner.succeed( 32 | dedent` 33 | Database structure analyzed, data model saved to: ${link(pathToFileURL(dataModelConfigPath).toString())} 34 | 35 | ${dim("The data model represents the structure of your database, and is used by Seed to generate realistic data 🔍")} 36 | ` + "\n", 37 | ); 38 | return { ok: true }; 39 | } catch (error) { 40 | spinner.fail(`Database analysis failed`); 41 | throw error; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/predict/listenForKeyPress.ts: -------------------------------------------------------------------------------- 1 | import { emitKeypressEvents } from "node:readline"; 2 | 3 | export const listenForKeyPress = (targetKey: string) => { 4 | let resolve = () => { 5 | return; 6 | }; 7 | 8 | let isCancelled = false; 9 | 10 | const promise = new Promise((resolver) => { 11 | resolve = () => { 12 | resolver(true); 13 | }; 14 | }); 15 | 16 | const listener = (_: unknown, key: { name: string } | undefined) => { 17 | if (key && key.name == targetKey) { 18 | cancel(); 19 | resolve(); 20 | } 21 | }; 22 | 23 | const cancel = () => { 24 | if (isCancelled) { 25 | return; 26 | } 27 | 28 | isCancelled = true; 29 | process.stdin.removeListener("keypress", listener); 30 | }; 31 | 32 | emitKeypressEvents(process.stdin); 33 | 34 | process.stdin.on("keypress", listener); 35 | 36 | if (process.stdin.isTTY) { 37 | process.stdin.setRawMode(true); 38 | } 39 | 40 | process.stdin.resume(); 41 | 42 | return { 43 | cancel, 44 | promise, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/predict/models.ts: -------------------------------------------------------------------------------- 1 | import { ChatGroq } from "@langchain/groq"; 2 | import { ChatOpenAI } from "@langchain/openai"; 3 | 4 | export const getCurrentModel = () => { 5 | const name = process.env["AI_MODEL_NAME"]; 6 | if (process.env["OPENAI_API_KEY"]) { 7 | return openAIModel(name); 8 | } 9 | if (process.env["GROQ_API_KEY"]) { 10 | return groqModel(name); 11 | } 12 | throw new Error("No API key found for Groq or OpenAI"); 13 | }; 14 | 15 | const openAIModel = ( 16 | modelName = "gpt-3.5-turbo-0125", 17 | apiKey = process.env["OPENAI_API_KEY"], 18 | ) => { 19 | if (!apiKey) { 20 | throw new Error("OPENAI_API_KEY is required to use OpenAI models"); 21 | } 22 | return new ChatOpenAI({ 23 | modelName, 24 | temperature: 0.9, 25 | openAIApiKey: apiKey, 26 | maxConcurrency: 10, 27 | }); 28 | }; 29 | 30 | const groqModel = ( 31 | modelName = "llama-3.1-8b-instant", 32 | apiKey = process.env["GROQ_API_KEY"], 33 | concurrency = process.env["AI_CONCURRENCY"] ?? "1", // Currently rate limits are quite low on Groq 34 | ) => { 35 | if (!apiKey) { 36 | throw new Error("GROQ_API_KEY is required to use Groq models"); 37 | } 38 | return new ChatGroq({ 39 | apiKey, 40 | model: modelName, 41 | maxConcurrency: parseInt(concurrency), 42 | }); 43 | }; 44 | 45 | interface TokenUsage { 46 | completionTokens?: string; 47 | promptTokens?: string; 48 | } 49 | 50 | export interface ResponseMetadata { 51 | tokenUsage?: TokenUsage; 52 | } 53 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/sync/sync.ts: -------------------------------------------------------------------------------- 1 | import { type Argv } from "yargs"; 2 | 3 | export function syncCommand(program: Argv) { 4 | return program.command( 5 | "sync", 6 | "Synchronize your database schema with Seed Client", 7 | { 8 | output: { 9 | hidden: true, 10 | alias: "o", 11 | describe: "A custom directory path to output the generated assets to", 12 | type: "string", 13 | }, 14 | }, 15 | async (args) => { 16 | const { syncHandler } = await import("./syncHandler.js"); 17 | await syncHandler(args); 18 | }, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/sync/syncHandler.ts: -------------------------------------------------------------------------------- 1 | import { bold, brightGreen, dim } from "#cli/lib/output.js"; 2 | import { dotSnapletPathExists, getDotSnapletPath } from "#config/dotSnaplet.js"; 3 | import { 4 | getProjectConfigPath, 5 | projectConfigExists, 6 | } from "#config/project/projectConfig.js"; 7 | import { 8 | getSeedConfigPath, 9 | seedConfigExists, 10 | } from "#config/seedConfig/seedConfig.js"; 11 | import { SnapletError } from "#core/utils.js"; 12 | import { generateHandler } from "../generate/generateHandler.js"; 13 | import { introspectHandler } from "../introspect/introspectHandler.js"; 14 | import { predictHandler } from "../predict/predictHandler.js"; 15 | 16 | async function ensureCanSync() { 17 | if (!(await seedConfigExists())) { 18 | throw new SnapletError("SEED_CONFIG_NOT_FOUND", { 19 | path: await getSeedConfigPath(), 20 | }); 21 | } 22 | 23 | if (!(await dotSnapletPathExists())) { 24 | throw new SnapletError("SNAPLET_FOLDER_NOT_FOUND", { 25 | path: await getDotSnapletPath(), 26 | }); 27 | } 28 | 29 | if (!(await projectConfigExists())) { 30 | throw new SnapletError("SNAPLET_PROJECT_CONFIG_NOT_FOUND", { 31 | path: await getProjectConfigPath(), 32 | }); 33 | } 34 | } 35 | 36 | export async function syncHandler(args: { isInit?: boolean; output?: string }) { 37 | await ensureCanSync(); 38 | await introspectHandler(); 39 | if (process.env["OPENAI_API_KEY"] ?? process.env["GROQ_API_KEY"]) { 40 | await predictHandler(); 41 | } else { 42 | console.log(` 43 | ${dim("Skipping AI-generated data...")} 44 | 45 | To get ${bold(" AI-generated data")}, you need to set either the ${brightGreen("OPENAI_API_KEY")} or ${brightGreen("GROQ_API_KEY")} environment variable.")} 46 | We also look for a .env file in the root of your project. 47 | 48 | To use a specific model, set the ${brightGreen("AI_MODEL_NAME")} environment variable. 49 | Example: ${brightGreen("AI_MODEL_NAME=gpt-4-mini")} 50 | `); 51 | } 52 | 53 | await generateHandler(args); 54 | } 55 | -------------------------------------------------------------------------------- /packages/seed/src/cli/commands/version.ts: -------------------------------------------------------------------------------- 1 | import { type Argv } from "yargs"; 2 | import { getVersion } from "#core/version.js"; 3 | 4 | export function versionOption(program: Argv) { 5 | program.version(getVersion()); 6 | } 7 | -------------------------------------------------------------------------------- /packages/seed/src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { gracefulExit } from "exit-hook"; 2 | import yargs from "yargs"; 3 | import { hideBin } from "yargs/helpers"; 4 | import { SnapletError, isError } from "#core/utils.js"; 5 | import { configOption } from "./commands/config.js"; 6 | import { generateCommand } from "./commands/generate/generate.js"; 7 | import { initCommand } from "./commands/init/init.js"; 8 | import { introspectCommand } from "./commands/introspect/introspect.js"; 9 | // import { predictCommand } from "./commands/predict/predict.js"; 10 | import { syncCommand } from "./commands/sync/sync.js"; 11 | import { versionOption } from "./commands/version.js"; 12 | import { debug } from "./lib/debug.js"; 13 | 14 | const program = yargs(hideBin(process.argv)).scriptName("npx @snaplet/seed"); 15 | 16 | configOption(program); 17 | initCommand(program); 18 | generateCommand(program); 19 | syncCommand(program); 20 | versionOption(program); 21 | introspectCommand(program); 22 | 23 | const handleFailure = (message: null | string, error: unknown) => { 24 | if (SnapletError.instanceof(error)) { 25 | console.error(error.toString()); 26 | } else if (message !== null) { 27 | console.error(message); 28 | } else if (isError(error)) { 29 | console.error(error.stack); 30 | } else if (error) { 31 | console.error(String(error)); 32 | debug(error); 33 | } 34 | }; 35 | 36 | try { 37 | await program.fail(handleFailure).parseAsync(); 38 | gracefulExit(); 39 | } catch (e) { 40 | // Error are already be handled by the fail handler nothing to do here 41 | // except to gracefully exit with an error code 42 | gracefulExit(1); 43 | } 44 | -------------------------------------------------------------------------------- /packages/seed/src/cli/lib/debug.ts: -------------------------------------------------------------------------------- 1 | import _debug from "debug"; 2 | 3 | export const debug = _debug("@snaplet/seed"); 4 | -------------------------------------------------------------------------------- /packages/seed/src/cli/lib/output.ts: -------------------------------------------------------------------------------- 1 | import ansiEscapes from "ansi-escapes"; 2 | import kleur from "kleur"; 3 | import ora from "ora"; 4 | import terminalLink from "terminal-link"; 5 | 6 | export function eraseLines(numberOfLines: number) { 7 | process.stdout.write(ansiEscapes.eraseLines(numberOfLines)); 8 | } 9 | 10 | export function link(text: string, url?: string) { 11 | return terminalLink(text, url ?? text); 12 | } 13 | 14 | export const spinner = ora(); 15 | 16 | export function bold(text: string) { 17 | return kleur.bold(text); 18 | } 19 | 20 | export function dim(text: string) { 21 | return kleur.dim(text); 22 | } 23 | 24 | export function error(text: string) { 25 | return kleur.red().bold(text); 26 | } 27 | 28 | export function brightGreen(text: string) { 29 | return kleur.bold().green(text); 30 | } 31 | -------------------------------------------------------------------------------- /packages/seed/src/config/__fixtures__/configs/project/invalid/.snaplet/config.json: -------------------------------------------------------------------------------- 1 | { 2 | malformed, json. 3 | } -------------------------------------------------------------------------------- /packages/seed/src/config/__fixtures__/configs/project/valid/.snaplet/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "qwerty123" 3 | } -------------------------------------------------------------------------------- /packages/seed/src/config/dataModelConfig.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import { readFile, writeFile } from "node:fs/promises"; 3 | import { join } from "node:path"; 4 | import { type DataModel } from "#core/dataModel/types.js"; 5 | import { SnapletError, jsonStringify } from "#core/utils.js"; 6 | import { ensureDotSnapletPath, getDotSnapletPath } from "./dotSnaplet.js"; 7 | 8 | export async function getDataModelConfig() { 9 | let dataModelConfig: DataModel | null = null; 10 | 11 | const dotSnapletPath = await getDotSnapletPath(); 12 | 13 | if (dotSnapletPath) { 14 | const dataModelConfigPath = join(dotSnapletPath, "dataModel.json"); 15 | if (existsSync(dataModelConfigPath)) { 16 | try { 17 | dataModelConfig = JSON.parse( 18 | await readFile(dataModelConfigPath, "utf8"), 19 | ) as DataModel; 20 | } catch (error) { 21 | throw new SnapletError("SEED_DATA_MODEL_INVALID", { 22 | path: dataModelConfigPath, 23 | error: error as Error, 24 | }); 25 | } 26 | } 27 | } 28 | 29 | return dataModelConfig; 30 | } 31 | 32 | export async function setDataModelConfig(dataModelConfig: DataModel) { 33 | const dotSnapletPath = await ensureDotSnapletPath(); 34 | 35 | const dataModelConfigPath = join(dotSnapletPath, "dataModel.json"); 36 | await writeFile( 37 | dataModelConfigPath, 38 | jsonStringify(dataModelConfig, undefined, 2), 39 | "utf8", 40 | ); 41 | } 42 | 43 | export async function getDataModelConfigPath() { 44 | const dotSnapletPath = await getDotSnapletPath(); 45 | if (!dotSnapletPath) { 46 | return null; 47 | } 48 | return join(dotSnapletPath, "dataModel.json"); 49 | } 50 | -------------------------------------------------------------------------------- /packages/seed/src/config/dotSnaplet.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import { mkdir } from "node:fs/promises"; 3 | import { dirname, join } from "node:path"; 4 | import { getSeedConfigPath } from "./seedConfig/seedConfig.js"; 5 | 6 | export async function getDotSnapletPath() { 7 | const seedConfigDirectory = dirname(await getSeedConfigPath()); 8 | 9 | return join(seedConfigDirectory, ".snaplet"); 10 | } 11 | 12 | export async function dotSnapletPathExists() { 13 | return existsSync(await getDotSnapletPath()); 14 | } 15 | 16 | export async function ensureDotSnapletPath() { 17 | let dotSnapletPath = await getDotSnapletPath(); 18 | 19 | if (!dotSnapletPath || !existsSync(dotSnapletPath)) { 20 | await mkdir(dotSnapletPath, { recursive: true }); 21 | } 22 | 23 | return dotSnapletPath; 24 | } 25 | -------------------------------------------------------------------------------- /packages/seed/src/config/project/projectConfig.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from "fs-extra"; 2 | import { existsSync } from "node:fs"; 3 | import { readFile, writeFile } from "node:fs/promises"; 4 | import path from "node:path"; 5 | import { z } from "zod"; 6 | import { type AdapterId, adapters } from "#adapters/index.js"; 7 | import { ensureDotSnapletPath, getDotSnapletPath } from "#config/dotSnaplet.js"; 8 | import { jsonStringify } from "#core/utils.js"; 9 | 10 | const projectConfigSchema = z.object({ 11 | projectDescription: z.string().optional(), 12 | adapter: z 13 | .string() 14 | .refine((v) => Object.keys(adapters).includes(v)) 15 | .optional() as z.ZodType, 16 | }); 17 | 18 | type ProjectConfig = z.infer; 19 | 20 | export async function getProjectConfigPath() { 21 | return path.join(await getDotSnapletPath(), "config.json"); 22 | } 23 | 24 | export async function getProjectConfig() { 25 | const projectConfigPath = await getProjectConfigPath(); 26 | 27 | if (await pathExists(projectConfigPath)) { 28 | return projectConfigSchema 29 | .passthrough() 30 | .parse(JSON.parse(await readFile(projectConfigPath, "utf8"))); 31 | } else { 32 | return {} as ProjectConfig; 33 | } 34 | } 35 | 36 | async function setProjectConfig(projectConfig: ProjectConfig) { 37 | await ensureDotSnapletPath(); 38 | 39 | const projectConfigPath = await getProjectConfigPath(); 40 | 41 | await writeFile( 42 | projectConfigPath, 43 | jsonStringify(projectConfig, undefined, 2), 44 | "utf8", 45 | ); 46 | } 47 | 48 | export async function updateProjectConfig( 49 | projectConfig: Partial, 50 | ) { 51 | const currentProjectConfig = await getProjectConfig(); 52 | 53 | const nextProjectConfig = { 54 | ...currentProjectConfig, 55 | ...projectConfig, 56 | }; 57 | 58 | await setProjectConfig(nextProjectConfig); 59 | } 60 | 61 | export async function projectConfigExists() { 62 | return existsSync(await getProjectConfigPath()); 63 | } 64 | -------------------------------------------------------------------------------- /packages/seed/src/config/seedConfig/adapterConfig.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | import { type DatabaseClient } from "#core/databaseClient.js"; 3 | 4 | export const adapterConfigSchema = z 5 | .function() 6 | .returns(z.union([z.promise(z.unknown()), z.unknown()])) as z.ZodType< 7 | () => DatabaseClient | Promise 8 | >; 9 | -------------------------------------------------------------------------------- /packages/seed/src/config/seedConfig/aliasConfig.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | type JsonPrimitive = boolean | null | number | string; 4 | type Nested = { [s: string]: Nested | V } | Array | V> | V; 5 | type Json = Nested; 6 | 7 | const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); 8 | const jsonSchema: z.ZodType = z.lazy(() => 9 | z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]), 10 | ); 11 | 12 | const scalarFieldSchema = z.object({ 13 | name: z.string(), 14 | type: z.string(), 15 | }); 16 | 17 | const objectFieldSchema = scalarFieldSchema.extend({ 18 | relationFromFields: z.array(z.string()), 19 | relationToFields: z.array(z.string()), 20 | }); 21 | 22 | const oppositeBaseNameMapSchema = z.record(z.string(), z.string()); 23 | 24 | export const aliasConfigSchema = z.object({ 25 | inflection: z 26 | .union([ 27 | z.object({ 28 | modelName: z.function().args(z.string()).returns(z.string()).optional(), 29 | scalarField: z 30 | .function() 31 | .args(scalarFieldSchema) 32 | .returns(z.string()) 33 | .optional(), 34 | parentField: z 35 | .function() 36 | .args(objectFieldSchema, oppositeBaseNameMapSchema) 37 | .returns(z.string()) 38 | .optional(), 39 | childField: z 40 | .function() 41 | .args(objectFieldSchema, objectFieldSchema, oppositeBaseNameMapSchema) 42 | .returns(z.string()) 43 | .optional(), 44 | oppositeBaseNameMap: oppositeBaseNameMapSchema.optional(), 45 | }), 46 | z.boolean(), 47 | ]) 48 | .optional() 49 | .default(false), 50 | override: z 51 | .record( 52 | z.string(), 53 | z.object({ 54 | name: z.string().optional(), 55 | fields: z.record(z.string(), z.string()).optional(), 56 | }), 57 | ) 58 | .optional(), 59 | }); 60 | -------------------------------------------------------------------------------- /packages/seed/src/config/seedConfig/defineConfig.ts: -------------------------------------------------------------------------------- 1 | import { type SeedConfig } from "./seedConfig.js"; 2 | 3 | export function defineConfig(config: SeedConfig) { 4 | return config; 5 | } 6 | -------------------------------------------------------------------------------- /packages/seed/src/config/seedConfig/fingerprint/config.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from "node:fs"; 2 | import { readFile } from "node:fs/promises"; 3 | import { join } from "node:path"; 4 | import { getDotSnapletPath } from "../../dotSnaplet.js"; 5 | import { type FingerprintConfig, fingerprintConfigSchema } from "./schemas.js"; 6 | 7 | export async function getFingerprintConfig() { 8 | let fingerprintConfig: FingerprintConfig = {}; 9 | 10 | const dotSnapletPath = await getDotSnapletPath(); 11 | 12 | if (dotSnapletPath) { 13 | const fingerprintConfigPath = join(dotSnapletPath, "fingerprint.json"); 14 | if (existsSync(fingerprintConfigPath)) { 15 | fingerprintConfig = fingerprintConfigSchema.parse( 16 | JSON.parse(await readFile(fingerprintConfigPath, "utf8")), 17 | ); 18 | } 19 | } 20 | 21 | return fingerprintConfig; 22 | } 23 | -------------------------------------------------------------------------------- /packages/seed/src/config/seedConfig/fingerprint/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const fingerprintConfigSchema = z.record( 4 | z.string().describe("modelName"), 5 | z.record( 6 | z.string().describe("modelField"), 7 | z.union([ 8 | z.object({ 9 | count: z.union([ 10 | z.number(), 11 | z.object({ min: z.number(), max: z.number() }), 12 | ]), 13 | }), 14 | z.object({ 15 | options: z.record(z.string(), z.any()), 16 | }), 17 | z.object({ 18 | schema: z.record(z.string(), z.any()).describe("jsonSchema"), 19 | }), 20 | z.object({ 21 | description: z.string().optional(), 22 | examples: z.array(z.string()).optional(), 23 | itemCount: z.number().optional(), 24 | }), 25 | ]), 26 | ), 27 | ); 28 | 29 | export type FingerprintConfig = z.infer; 30 | -------------------------------------------------------------------------------- /packages/seed/src/config/seedConfig/selectConfig.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const selectConfigSchema = z.array(z.string()); 4 | 5 | export type SelectConfig = z.infer; 6 | -------------------------------------------------------------------------------- /packages/seed/src/core/client/types.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { type Constraints, type PlanOptions } from "../plan/types.js"; 3 | import { type Store } from "../store/store.js"; 4 | import { type UserModels } from "../userModels/types.js"; 5 | 6 | export interface SeedClientOptions { 7 | adapter?: DatabaseClient; 8 | connect?: PlanOptions["connect"]; 9 | dryRun?: boolean; 10 | models?: UserModels; 11 | } 12 | 13 | export interface ClientState { 14 | constraints: Constraints; 15 | seeds: Record; 16 | store: Store; 17 | } 18 | -------------------------------------------------------------------------------- /packages/seed/src/core/client/utils.ts: -------------------------------------------------------------------------------- 1 | import { type SelectConfig } from "#config/seedConfig/selectConfig.js"; 2 | import { computeIncludedTables } from "#core/dataModel/select.js"; 3 | import { type DataModelModel } from "#core/dataModel/types.js"; 4 | 5 | export function filterModelsBySelectConfig( 6 | models: Array, 7 | selectConfig?: SelectConfig, 8 | ) { 9 | let filteredModels = Object.values(models); 10 | if (selectConfig !== undefined) { 11 | const tableIds = Object.values(models).map((model) => model.id); 12 | const includedTableIds = new Set( 13 | computeIncludedTables(tableIds, selectConfig), 14 | ); 15 | filteredModels = filteredModels.filter((model) => 16 | includedTableIds.has(model.id), 17 | ); 18 | } 19 | return filteredModels; 20 | } 21 | -------------------------------------------------------------------------------- /packages/seed/src/core/codegen/userModels/generateJsonField.ts: -------------------------------------------------------------------------------- 1 | import * as JsonSchemaLibrary from "json-schema-library"; 2 | 3 | const { Draft06 } = JsonSchemaLibrary; 4 | 5 | type JsonSchema = Record; 6 | 7 | export function generateJsonField(props: { schema: JsonSchema }) { 8 | const patchedSchema = patchSchema(props.schema); 9 | 10 | const jsonSchema = new Draft06(patchedSchema, { 11 | templateDefaultOptions: { 12 | // can be activated to generate values for optional properties 13 | addOptionalProps: true, 14 | }, 15 | }); 16 | 17 | const jsonTemplate = jsonSchema.getTemplate() as unknown; 18 | 19 | return traverseJson([], jsonTemplate); 20 | } 21 | 22 | /** 23 | * Add `minItems: 1` to all array properties to generate one item element per array 24 | */ 25 | function patchSchema(schema: JsonSchema) { 26 | for (const key of Object.keys(schema)) { 27 | if (key === "type" && schema[key] === "array") { 28 | schema["minItems"] = 1; 29 | } 30 | if (typeof schema[key] === "object" && schema[key] !== null) { 31 | schema[key] = patchSchema(schema[key] as JsonSchema); 32 | } 33 | } 34 | return schema; 35 | } 36 | 37 | function traverseJson(path: Array = [], node: unknown): string { 38 | if (Array.isArray(node)) { 39 | return ( 40 | "[" + 41 | node 42 | .map((item, index) => traverseJson([...path, index.toString()], item)) 43 | .join(", ") + 44 | "]" 45 | ); 46 | } else if (typeof node === "object" && node !== null) { 47 | const properties = Object.entries(node).map( 48 | ([key, value]) => `'${key}': ${traverseJson([...path, key], value)}`, 49 | ); 50 | return `{${properties.join(", ")}}`; 51 | } else { 52 | return getCopycatMethod(node, path); 53 | } 54 | } 55 | 56 | function getCopycatMethod(value: unknown, path: Array): string { 57 | const pathString = `seed + "/${path.join("/")}"`; 58 | switch (typeof value) { 59 | case "string": 60 | return `copycat.word(${pathString})`; 61 | case "number": 62 | return `copycat.int(${pathString})`; 63 | default: 64 | return `copycat.word(${pathString})`; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/seed/src/core/data/data.ts: -------------------------------------------------------------------------------- 1 | import { mapValues } from "remeda"; 2 | import { isInstanceOf } from "../utils.js"; 3 | import { type Json, type Serializable } from "./types.js"; 4 | 5 | export const serializeValue = (value: Serializable): Json | undefined => { 6 | return typeof value === "bigint" 7 | ? value.toString() 8 | : isInstanceOf(value, Date) 9 | ? value.toISOString() 10 | : value; 11 | }; 12 | 13 | export const serializeModelValues = ( 14 | model: Record, 15 | ): Record => mapValues(model, serializeValue); 16 | -------------------------------------------------------------------------------- /packages/seed/src/core/data/types.ts: -------------------------------------------------------------------------------- 1 | type JsonPrimitive = boolean | null | number | string; 2 | 3 | export type Json = { [key: string]: Json } | Array | JsonPrimitive; 4 | 5 | type SerializablePrimitive = 6 | | Date 7 | | bigint 8 | | boolean 9 | | null 10 | | number 11 | | string 12 | | undefined; 13 | 14 | export type Serializable = Json | SerializablePrimitive; 15 | -------------------------------------------------------------------------------- /packages/seed/src/core/dataModel/shouldGenerateFieldValue.ts: -------------------------------------------------------------------------------- 1 | import { type DataModelField } from "./types.js"; 2 | 3 | export const shouldGenerateFieldValue = (field: DataModelField): boolean => { 4 | if ( 5 | // If the field is a relation to another table, we don't want to generate the value we will connect 6 | // with existing value from the target table 7 | field.kind !== "scalar" || 8 | // * If field is a generated non-id field, it is always generated by the database 9 | // * If the field is both a sequence and id, its default value must be null so we can overide it 10 | // with the sequence generator in the plan 11 | (!field.isId && field.isGenerated) || 12 | (field.isId && field.sequence) 13 | ) { 14 | return false; 15 | } 16 | 17 | return true; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/seed/src/core/dataModel/types.ts: -------------------------------------------------------------------------------- 1 | export interface DataModel { 2 | enums: Record; 3 | models: Record; 4 | } 5 | 6 | interface DataModelEnum { 7 | schemaName?: string; 8 | values: Array<{ name: string }>; 9 | } 10 | 11 | export interface DataModelModel { 12 | fields: Array; 13 | id: string; 14 | schemaName?: string; 15 | tableName: string; 16 | uniqueConstraints: Array; 17 | } 18 | 19 | export interface DataModelUniqueConstraint { 20 | fields: Array; 21 | name: string; 22 | nullNotDistinct?: boolean; 23 | } 24 | 25 | export interface DataModelSequence { 26 | identifier: null | string; 27 | increment: number; 28 | start?: number; 29 | } 30 | 31 | export type DataModelField = DataModelObjectField | DataModelScalarField; 32 | 33 | export type DataModelObjectField = { 34 | kind: "object"; 35 | relationFromFields: Array; 36 | relationName: string; 37 | relationToFields: Array; 38 | } & DataModelCommonFieldProps; 39 | 40 | export type DataModelScalarField = { 41 | columnName: string; 42 | id: string; 43 | kind: "scalar"; 44 | maxLength?: null | number; 45 | } & DataModelCommonFieldProps; 46 | 47 | interface DataModelCommonFieldProps { 48 | hasDefaultValue: boolean; 49 | isGenerated: boolean; 50 | isId: boolean; 51 | isList: boolean; 52 | isRequired: boolean; 53 | name: string; 54 | sequence: DataModelSequence | false; 55 | type: string; 56 | } 57 | -------------------------------------------------------------------------------- /packages/seed/src/core/dataModel/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DataModelField, 3 | type DataModelObjectField, 4 | type DataModelScalarField, 5 | } from "./types.js"; 6 | 7 | export function groupFields(fields: Array) { 8 | const groupedFields = { 9 | scalars: [] as Array, 10 | parents: [] as Array, 11 | children: [] as Array, 12 | }; 13 | 14 | for (const field of fields) { 15 | if (field.kind === "scalar") { 16 | groupedFields.scalars.push(field); 17 | } else if (field.relationFromFields.length > 0) { 18 | groupedFields.parents.push(field); 19 | } else if (field.relationFromFields.length === 0) { 20 | groupedFields.children.push(field); 21 | } 22 | } 23 | 24 | return groupedFields; 25 | } 26 | -------------------------------------------------------------------------------- /packages/seed/src/core/databaseClient.ts: -------------------------------------------------------------------------------- 1 | export type DatabaseClientDialect = "mysql" | "postgres" | "sqlite"; 2 | export abstract class DatabaseClient { 3 | constructor(public client: T) {} 4 | abstract execute(query: string): Promise; 5 | abstract query( 6 | query: string, 7 | values?: Array, 8 | ): Promise>; 9 | } 10 | -------------------------------------------------------------------------------- /packages/seed/src/core/dialect/generateConfigTypes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateConfigTypes as _generateConfigTypes, 3 | type FingerprintTypes, 4 | } from "#core/codegen/generateConfigTypes.js"; 5 | import { type DataModel, type DataModelField } from "#core/dataModel/types.js"; 6 | import { 7 | LLM_PREDICTABLE_TYPES, 8 | SQL_DATE_TYPES, 9 | SQL_JSON_TYPES, 10 | SQL_NUMBER_TYPES, 11 | } from "./utils.js"; 12 | 13 | export function generateConfigTypes(props: { 14 | dataModel: DataModel; 15 | rawDataModel?: DataModel; 16 | }) { 17 | return _generateConfigTypes({ 18 | ...props, 19 | computeFingerprintFieldTypeName, 20 | }); 21 | } 22 | 23 | function computeFingerprintFieldTypeName( 24 | field: DataModelField, 25 | ): FingerprintTypes { 26 | if (field.kind === "object") { 27 | return "FingerprintRelationField"; 28 | } 29 | 30 | if (SQL_JSON_TYPES.has(field.type)) { 31 | return "FingerprintJsonField"; 32 | } 33 | 34 | if (SQL_DATE_TYPES.has(field.type)) { 35 | return "FingerprintDateField"; 36 | } 37 | 38 | if (SQL_NUMBER_TYPES.has(field.type)) { 39 | return "FingerprintNumberField"; 40 | } 41 | 42 | if (LLM_PREDICTABLE_TYPES.has(field.type)) { 43 | return "FingerprintLLMField"; 44 | } 45 | 46 | return null; 47 | } 48 | -------------------------------------------------------------------------------- /packages/seed/src/core/dialect/groupParentsChildrenRelations.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from "./utils.js"; 2 | 3 | export function groupParentsChildrenRelations< 4 | C extends { 5 | fkColumn: string; 6 | nullable: boolean; 7 | targetColumn: string; 8 | }, 9 | T extends { 10 | fkTable: string; 11 | id: string; 12 | keys: Array; 13 | targetTable: string; 14 | }, 15 | >(databaseRelationships: Array, tableIds: Array) { 16 | const tablesRelationships = new Map< 17 | string, 18 | { 19 | children: Array; 20 | parents: Array; 21 | } 22 | >(); 23 | const children = groupBy(databaseRelationships, (f) => f.targetTable); 24 | const parents = groupBy(databaseRelationships, (f) => f.fkTable); 25 | for (const tableId of tableIds) { 26 | tablesRelationships.set(tableId, { 27 | parents: parents[tableId] ?? [], 28 | children: children[tableId] ?? [], 29 | }); 30 | } 31 | return tablesRelationships; 32 | } 33 | -------------------------------------------------------------------------------- /packages/seed/src/core/dialect/types.ts: -------------------------------------------------------------------------------- 1 | import { type SeedConfig } from "#config/seedConfig/seedConfig.js"; 2 | import { type DataModel } from "#core/dataModel/types.js"; 3 | import { type Fingerprint } from "#core/fingerprint/types.js"; 4 | import { type Templates } from "#core/userModels/templates/types.js"; 5 | import { type Shape } from "#trpc/shapes.js"; 6 | import { type DatabaseClient } from "../databaseClient.js"; 7 | 8 | export type NestedType = string; 9 | 10 | type GetDataModel = (client: DatabaseClient) => Promise; 11 | 12 | export interface Dialect { 13 | determineShapeFromType: DetermineShapeFromType; 14 | generateClientTypes: GenerateClientTypes; 15 | generateConfigTypes: GenerateConfigTypes; 16 | getDataModel: GetDataModel; 17 | id: string; 18 | templates: Templates; 19 | } 20 | 21 | // context(justinvdm, 6 Mar 2024): A `null` result means no shape was determined (so we should ask 22 | // the API for the shape), while `__DEFAULT` means we have intentionally opted out of determining a 23 | // shape and we should instead use the default template for the type 24 | export type DetermineShapeFromType = (type: string) => Shape | null; 25 | 26 | type GenerateClientTypes = (props: { 27 | dataModel: DataModel; 28 | fingerprint?: Fingerprint; 29 | seedConfig?: SeedConfig; 30 | }) => Promise | string; 31 | 32 | type GenerateConfigTypes = (props: { 33 | dataModel: DataModel; 34 | rawDataModel: DataModel; 35 | }) => Promise | string; 36 | -------------------------------------------------------------------------------- /packages/seed/src/core/dialect/unpackNestedType.ts: -------------------------------------------------------------------------------- 1 | import { type NestedType } from "./types.js"; 2 | 3 | export const unpackNestedType = ( 4 | type: NestedType | Type, 5 | ): [Type, number] => { 6 | const [primitive, ...rest] = type.split("[]"); 7 | return [primitive as Type, rest.length]; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/seed/src/core/fingerprint/fingerprint.ts: -------------------------------------------------------------------------------- 1 | import { camelize } from "inflection"; 2 | import { 3 | FetchingJSONSchemaStore, 4 | InputData, 5 | JSONSchemaInput, 6 | quicktype, 7 | } from "quicktype-core"; 8 | import { mergeDeep } from "remeda"; 9 | import { getFingerprintConfig } from "../../config/seedConfig/fingerprint/config.js"; 10 | import { getSeedConfig } from "../../config/seedConfig/seedConfig.js"; 11 | import { 12 | type Fingerprint, 13 | type FingerprintField, 14 | type FingerprintJsonField, 15 | type FingerprintOptionsField, 16 | } from "./types.js"; 17 | 18 | export function isJsonField( 19 | field: FingerprintField, 20 | ): field is FingerprintJsonField { 21 | return "schema" in field; 22 | } 23 | 24 | export function isOptionsField( 25 | field: FingerprintField, 26 | ): field is FingerprintOptionsField { 27 | return "options" in field; 28 | } 29 | /** 30 | * @public will be used during code generation 31 | */ 32 | export async function getFingerprint(): Promise { 33 | const snapletConfig = await getSeedConfig(); 34 | const fingerprintConfig = await getFingerprintConfig(); 35 | 36 | return mergeDeep(fingerprintConfig, snapletConfig.fingerprint ?? {}); 37 | } 38 | 39 | export async function jsonSchemaToTypescriptType( 40 | namespace: string, 41 | schema: string, 42 | ) { 43 | const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); 44 | 45 | await schemaInput.addSource({ name: "default", schema }); 46 | 47 | const inputData = new InputData(); 48 | inputData.addInput(schemaInput); 49 | 50 | const typescriptType = await quicktype({ 51 | inputData, 52 | lang: "typescript", 53 | indentation: " ", 54 | rendererOptions: { 55 | "just-types": true, 56 | }, 57 | }); 58 | 59 | const types = typescriptType.lines.join("\n"); 60 | 61 | const standardNamespace = camelize(namespace); 62 | 63 | return { 64 | name: `${standardNamespace}JsonField.Default`, 65 | types: `declare namespace ${standardNamespace}JsonField { 66 | ${types}}`, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /packages/seed/src/core/fingerprint/types.ts: -------------------------------------------------------------------------------- 1 | export type Fingerprint = Record>; 2 | 3 | export type FingerprintField = 4 | | FingerprintJsonField 5 | | FingerprintLLMField 6 | | FingerprintOptionsField 7 | | FingerprintPromptField 8 | | FingerprintRelationshipField; 9 | 10 | export interface FingerprintJsonField { 11 | schema: Record; 12 | } 13 | 14 | export interface FingerprintOptionsField { 15 | options: Record; 16 | } 17 | 18 | interface FingerprintLLMField { 19 | description?: string; 20 | } 21 | 22 | interface FingerprintRelationshipField { 23 | count: { max: number; min: number } | number; 24 | } 25 | 26 | interface FingerprintPromptField { 27 | description?: string; 28 | examples?: Array; 29 | itemCount?: number; 30 | } 31 | -------------------------------------------------------------------------------- /packages/seed/src/core/predictions/computePredictionContext.ts: -------------------------------------------------------------------------------- 1 | import { getSeedConfig } from "#config/seedConfig/seedConfig.js"; 2 | import { getDataModel } from "#core/dataModel/dataModel.js"; 3 | import { getDialect } from "#dialects/getDialect.js"; 4 | import { columnsToPredict } from "./utils.js"; 5 | 6 | export const computePredictionContext = async () => { 7 | const dataModel = await getDataModel(); 8 | const dialect = await getDialect(); 9 | const seedConfig = await getSeedConfig(); 10 | 11 | const columns = columnsToPredict( 12 | dataModel, 13 | dialect.determineShapeFromType, 14 | seedConfig.fingerprint ?? {}, 15 | ); 16 | 17 | return { 18 | dataModel, 19 | columns, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/seed/src/core/predictions/shapeExamples/getDataExamples.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from "fs-extra/esm"; 2 | import { readFile } from "node:fs/promises"; 3 | import { join } from "node:path"; 4 | import { getDotSnapletPath } from "#config/dotSnaplet.js"; 5 | import { FILES } from "../../codegen/codegen.js"; 6 | import { type DataExample } from "../types.js"; 7 | 8 | export async function getDataExamples(): Promise> { 9 | let dataExamples: Array = []; 10 | 11 | const dotSnapletPath = await getDotSnapletPath(); 12 | 13 | if (dotSnapletPath) { 14 | const dataExamplesPath = join(dotSnapletPath, FILES.DATA_EXAMPLES.name); 15 | 16 | if (await pathExists(dataExamplesPath)) { 17 | dataExamples = JSON.parse( 18 | await readFile(dataExamplesPath, "utf8"), 19 | ) as Array; 20 | } 21 | } 22 | 23 | return dataExamples; 24 | } 25 | -------------------------------------------------------------------------------- /packages/seed/src/core/predictions/shapeExamples/setDataExamples.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from "node:fs/promises"; 2 | import { join } from "node:path"; 3 | import { ensureDotSnapletPath } from "#config/dotSnaplet.js"; 4 | import { type DataExample } from "#core/predictions/types.js"; 5 | import { jsonStringify } from "#core/utils.js"; 6 | import { FILES } from "../../codegen/codegen.js"; 7 | 8 | export async function setDataExamples(shapeExamples: Array) { 9 | const dotSnapletPath = await ensureDotSnapletPath(); 10 | 11 | const shapeExamplesPath = join(dotSnapletPath, FILES.DATA_EXAMPLES.name); 12 | 13 | await writeFile( 14 | shapeExamplesPath, 15 | jsonStringify(shapeExamples, undefined, 2), 16 | "utf8", 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/seed/src/core/predictions/shapePredictions/getShapePredictions.ts: -------------------------------------------------------------------------------- 1 | import { pathExists } from "fs-extra/esm"; 2 | import { readFile } from "node:fs/promises"; 3 | import { join } from "node:path"; 4 | import { getDotSnapletPath } from "#config/dotSnaplet.js"; 5 | import { type ShapePredictions } from "../types.js"; 6 | 7 | export async function getShapePredictions(): Promise { 8 | let shapePredictions: ShapePredictions = []; 9 | 10 | const dotSnapletPath = await getDotSnapletPath(); 11 | 12 | if (dotSnapletPath) { 13 | const shapePredictionsPath = join(dotSnapletPath, "shapePredictions.json"); 14 | 15 | if (await pathExists(shapePredictionsPath)) { 16 | shapePredictions = JSON.parse( 17 | await readFile(shapePredictionsPath, "utf8"), 18 | ) as ShapePredictions; 19 | } 20 | } 21 | 22 | return shapePredictions; 23 | } 24 | -------------------------------------------------------------------------------- /packages/seed/src/core/predictions/types.ts: -------------------------------------------------------------------------------- 1 | import { type TableShapePredictions } from "#trpc/shapes.js"; 2 | 3 | export type ShapePredictions = Array; 4 | 5 | export interface DataExample { 6 | description: string; 7 | examples: Array; 8 | input: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/seed/src/core/predictions/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs-extra"; 2 | import path from "node:path"; 3 | import { describe, expect, test } from "vitest"; 4 | import { determineShapeFromType as pgDetermineShapeFromType } from "#dialects/postgres/determineShapeFromType.js"; 5 | import { type DataModel } from "../dataModel/types.js"; 6 | import { columnsToPredict } from "./utils.js"; 7 | 8 | describe("columnsToPredict", () => { 9 | const postgresDataModel = JSON.parse( 10 | readFileSync( 11 | path.resolve( 12 | path.join( 13 | __dirname, 14 | "../dataModel/__fixtures__/basicPostgresDataModel.json", 15 | ), 16 | ), 17 | "utf-8", 18 | ), 19 | ) as DataModel; 20 | test("should exlude all non text and ids / relations / constraints columns", () => { 21 | expect( 22 | columnsToPredict(postgresDataModel, pgDetermineShapeFromType), 23 | ).toEqual( 24 | expect.arrayContaining([ 25 | expect.objectContaining({ 26 | columnName: "title", 27 | pgType: "text", 28 | schemaName: "public", 29 | tableName: "Post", 30 | }), 31 | expect.objectContaining({ 32 | columnName: "content", 33 | pgType: "text", 34 | schemaName: "public", 35 | tableName: "Post", 36 | }), 37 | expect.objectContaining({ 38 | columnName: "name", 39 | pgType: "text", 40 | schemaName: "public", 41 | tableName: "Tag", 42 | }), 43 | expect.objectContaining({ 44 | columnName: "name", 45 | pgType: "text", 46 | schemaName: "public", 47 | tableName: "User", 48 | }), 49 | expect.objectContaining({ 50 | columnName: "checksum", 51 | pgType: "varchar", 52 | schemaName: "public", 53 | tableName: "_prisma_migrations", 54 | }), 55 | expect.objectContaining({ 56 | columnName: "migration_name", 57 | pgType: "varchar", 58 | schemaName: "public", 59 | tableName: "_prisma_migrations", 60 | }), 61 | expect.objectContaining({ 62 | columnName: "logs", 63 | pgType: "text", 64 | schemaName: "public", 65 | tableName: "_prisma_migrations", 66 | }), 67 | ]), 68 | ); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /packages/seed/src/core/store/store.ts: -------------------------------------------------------------------------------- 1 | import { type DataModel } from "../dataModel/types.js"; 2 | import { type ModelData } from "../plan/types.js"; 3 | 4 | export interface Store { 5 | _store: Record>; 6 | add(model: string, value: ModelData): void; 7 | toSQL(): Array; 8 | } 9 | 10 | export abstract class StoreBase implements Store { 11 | _store: Record>; 12 | public readonly dataModel: DataModel; 13 | 14 | constructor(dataModel: DataModel) { 15 | this.dataModel = dataModel; 16 | this._store = Object.fromEntries( 17 | Object.keys(dataModel.models).map((modelName) => [modelName, []]), 18 | ); 19 | } 20 | 21 | add(model: string, value: ModelData) { 22 | this._store[model].push(value); 23 | } 24 | 25 | abstract toSQL(): Array; 26 | } 27 | -------------------------------------------------------------------------------- /packages/seed/src/core/symbols.ts: -------------------------------------------------------------------------------- 1 | const FallbackSymbol = "Snaplet:Fallback"; 2 | 3 | export { FallbackSymbol }; 4 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/encloseValueInArray.ts: -------------------------------------------------------------------------------- 1 | export function encloseValueInArray(value: string, dimensions: number) { 2 | if (dimensions === 0) { 3 | return value; 4 | } 5 | 6 | return Array(dimensions) 7 | .fill(undefined) 8 | .reduce((acc) => `[${acc}]`, value); 9 | } 10 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/categories/bits.ts: -------------------------------------------------------------------------------- 1 | import { type TypeTemplates } from "../types.js"; 2 | 3 | export const bits: TypeTemplates = ({ input, maxLength }) => ` 4 | (() => { 5 | const len = ${maxLength} || 1 6 | let bits = '' 7 | for (let i = 0; i < len; i++) { 8 | bits += copycat.oneOf(${input} + i, ['0', '1']) 9 | } 10 | return bits 11 | })() 12 | `; 13 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/categories/floats.ts: -------------------------------------------------------------------------------- 1 | import { copycatTemplate } from "../copycat.js"; 2 | import { type TypeTemplates } from "../types.js"; 3 | 4 | export const floats = (bytes: number): TypeTemplates => { 5 | return { 6 | LOCATION_LATITUDE: copycatTemplate("int", { 7 | options: { min: -90, max: 90 }, 8 | }), 9 | LOCATION_LONGITUDE: copycatTemplate("int", { 10 | options: { min: -90, max: 90 }, 11 | }), 12 | __DEFAULT: copycatTemplate("float", { 13 | options: { min: 0, max: Math.pow(2, bytes) }, 14 | }), 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/categories/geometry.ts: -------------------------------------------------------------------------------- 1 | import { type TypeTemplates } from "../types.js"; 2 | 3 | export const line: TypeTemplates = ({ input }) => ` 4 | (() => { 5 | let options = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 6 | const points = [] 7 | 8 | for (let i = 0; i < 4; i++) { 9 | const value = copycat.oneOf(${input}, options) 10 | points.push(value) 11 | options = options.filter((p) => p !== value) 12 | } 13 | 14 | return '(' + points[0] + ', ' + points[1] + '), (' + points[2] + ', ' + points[3] + ')' 15 | })() 16 | `; 17 | 18 | export const circle: TypeTemplates = ({ input }) => ` 19 | (() => { 20 | let options = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 21 | const points = [] 22 | 23 | for (let i = 0; i < 3; i++) { 24 | const value = copycat.oneOf(${input}, options) 25 | points.push(value) 26 | options = options.filter((p) => p !== value) 27 | } 28 | 29 | return '((' + points[0] + ', ' + points[1] + ' ), ' + points[2] + ' )' 30 | })() 31 | `; 32 | 33 | export const point: TypeTemplates = ({ input }) => 34 | `'(' + copycat.int(${input}, { max: 10 }) + ',' + copycat.int(${input}, { max: 10 }) + ')'`; 35 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/categories/integers.ts: -------------------------------------------------------------------------------- 1 | import { copycatTemplate } from "../copycat.js"; 2 | import { type TypeTemplates } from "../types.js"; 3 | 4 | export const integers = (bytes: number): TypeTemplates => { 5 | return { 6 | INDEX: copycatTemplate("int", { 7 | options: { min: 1, max: Math.pow(bytes, 8) - 1 }, 8 | }), 9 | LOCATION_LATITUDE: copycatTemplate("int", { 10 | options: { min: -90, max: 90 }, 11 | }), 12 | LOCATION_LONGITUDE: copycatTemplate("int", { 13 | options: { min: -90, max: 90 }, 14 | }), 15 | __DEFAULT: copycatTemplate("int", { 16 | options: { min: 0, max: Math.pow(bytes, 8) - 1 }, 17 | }), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/codegen.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { generateCodeFromTemplate } from "./codegen.js"; 3 | 4 | describe("generateCodeFromTemplate", () => { 5 | test("generates code from template", () => { 6 | expect( 7 | generateCodeFromTemplate({ 8 | input: "foo", 9 | type: "text", 10 | maxLength: null, 11 | shape: "PERSON_FIRST_NAME", 12 | optionsInput: null, 13 | templates: { 14 | text: { 15 | PERSON_FIRST_NAME: () => '"bar"', 16 | __DEFAULT: () => '"baz"', 17 | }, 18 | }, 19 | }), 20 | ).toEqual('"bar"'); 21 | }); 22 | 23 | test("uses default template if there is no matching shape", () => { 24 | expect( 25 | generateCodeFromTemplate({ 26 | input: "foo", 27 | type: "text", 28 | maxLength: null, 29 | shape: "PERSON_FIRST_NAME", 30 | optionsInput: null, 31 | templates: { 32 | text: { 33 | __DEFAULT: () => '"baz"', 34 | }, 35 | }, 36 | }), 37 | ).toEqual('"baz"'); 38 | }); 39 | 40 | test("uses default template if there is no shape", () => { 41 | expect( 42 | generateCodeFromTemplate({ 43 | input: "foo", 44 | type: "text", 45 | maxLength: null, 46 | shape: null, 47 | optionsInput: null, 48 | templates: { 49 | text: { 50 | PERSON_FIRST_NAME: () => '"bar"', 51 | __DEFAULT: () => '"baz"', 52 | }, 53 | }, 54 | }), 55 | ).toEqual('"baz"'); 56 | }); 57 | 58 | test("supports array types", () => { 59 | expect( 60 | generateCodeFromTemplate({ 61 | input: "foo", 62 | type: "text[]", 63 | maxLength: null, 64 | shape: null, 65 | optionsInput: null, 66 | templates: { 67 | text: { 68 | __DEFAULT: () => '"bar"', 69 | }, 70 | }, 71 | }), 72 | ).toEqual('["bar"]'); 73 | 74 | expect( 75 | generateCodeFromTemplate({ 76 | input: "foo", 77 | type: "text[][]", 78 | maxLength: null, 79 | shape: null, 80 | optionsInput: null, 81 | templates: { 82 | text: { 83 | __DEFAULT: () => '"bar"', 84 | }, 85 | }, 86 | }), 87 | ).toEqual('[["bar"]]'); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/codegen.ts: -------------------------------------------------------------------------------- 1 | import { type NestedType } from "#core/dialect/types.js"; 2 | import { unpackNestedType } from "#core/dialect/unpackNestedType.js"; 3 | import { type Shape } from "#trpc/shapes.js"; 4 | import { encloseValueInArray } from "../encloseValueInArray.js"; 5 | import { 6 | type TemplateContext, 7 | type TemplateFn, 8 | type TemplateInput, 9 | type Templates, 10 | } from "./types.js"; 11 | 12 | export const generateCodeFromTemplate = (props: { 13 | input: TemplateInput; 14 | maxLength: null | number; 15 | optionsInput: null | string; 16 | shape: Shape | null; 17 | templates: Templates; 18 | type: NestedType | Type; 19 | }) => { 20 | const { 21 | input, 22 | maxLength, 23 | shape, 24 | templates, 25 | type: wrappedType, 26 | optionsInput, 27 | } = props; 28 | 29 | const [type, dimensions] = unpackNestedType(wrappedType); 30 | 31 | const context: TemplateContext = { 32 | input, 33 | type, 34 | maxLength, 35 | shape, 36 | optionsInput, 37 | }; 38 | 39 | let result: null | string = null; 40 | const shapeTemplates = templates[type]; 41 | if (!shapeTemplates) { 42 | result = null; 43 | } else if (typeof shapeTemplates === "function") { 44 | result = shapeTemplates(context); 45 | } else { 46 | let fn: TemplateFn | null | undefined; 47 | 48 | if (!shape || !shapeTemplates[shape]) { 49 | fn = shapeTemplates.__DEFAULT ?? null; 50 | } else { 51 | fn = shapeTemplates[shape]; 52 | } 53 | 54 | if (fn) { 55 | result = fn(context); 56 | } 57 | } 58 | 59 | return result ? encloseValueInArray(result, dimensions) : null; 60 | }; 61 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/testing.ts: -------------------------------------------------------------------------------- 1 | import { copycat } from "@snaplet/copycat"; 2 | import vm from "node:vm"; 3 | import { type TemplateContext } from "./types.js"; 4 | 5 | export const createTemplateContext = (): TemplateContext => ({ 6 | type: "text", 7 | shape: "EMAIL", 8 | input: "input", 9 | maxLength: null, 10 | optionsInput: null, 11 | }); 12 | 13 | export const runTemplateCode = (context: TemplateContext, code: string) => { 14 | try { 15 | return { 16 | success: true, 17 | value: vm.runInNewContext(code, { 18 | copycat, 19 | Date, 20 | [String(context.input)]: "inputValue", 21 | }) as unknown, 22 | }; 23 | } catch (error) { 24 | return { 25 | success: false, 26 | error, 27 | }; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/templates/types.ts: -------------------------------------------------------------------------------- 1 | import { type Shape } from "#trpc/shapes.js"; 2 | import { type ShapeExtra } from "./shapeExtra.js"; 3 | 4 | export interface TemplateContext { 5 | input: TemplateInput; 6 | maxLength: null | number; 7 | optionsInput: null | string; 8 | shape: Shape | null; 9 | type: Type; 10 | } 11 | 12 | export type TemplateInput = string; 13 | 14 | type TemplateResult = null | string; 15 | 16 | export type TemplateFn = (api: TemplateContext) => TemplateResult; 17 | 18 | export type TypeTemplates = TemplateFn | TypeTemplatesRecord; 19 | 20 | type TypeTemplatesRecord = Partial< 21 | Record<"__DEFAULT" | Shape | ShapeExtra, TemplateFn | null> 22 | >; 23 | 24 | export type Templates = Partial< 25 | Record 26 | >; 27 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/types.ts: -------------------------------------------------------------------------------- 1 | import { type ConnectCallback, type ScalarField } from "../plan/types.js"; 2 | 3 | export type UserModelsData = Record; 4 | 5 | export type UserModels = Record< 6 | string, 7 | { connect?: ConnectCallback; data?: UserModelsData } 8 | >; 9 | -------------------------------------------------------------------------------- /packages/seed/src/core/userModels/userModels.ts: -------------------------------------------------------------------------------- 1 | import { dedupePreferLast } from "../utils.js"; 2 | import { type UserModels, type UserModelsData } from "./types.js"; 3 | 4 | /** 5 | * Utility function to merge two user models, userModels2 will override userModels1 fields 6 | * It's used to: 7 | * - merge the static userModels computed from AI with the userModels provided in the client constructor as an option 8 | * - merge the userModels stored in the client with the userModels provided in a plan as an option 9 | */ 10 | export function mergeUserModels( 11 | userModels1: UserModels, 12 | userModels2: UserModels, 13 | ) { 14 | const mergedUserModels: UserModels = {}; 15 | 16 | const userModels1Keys = Object.keys(userModels1); 17 | const userModels2Keys = Object.keys(userModels2); 18 | 19 | userModels1Keys.forEach((modelName) => { 20 | const userModel1 = userModels1[modelName] ?? {}; 21 | const userModel2 = userModels2[modelName] ?? {}; 22 | 23 | mergedUserModels[modelName] = { 24 | data: mergeUserModelsData(userModel1.data ?? {}, userModel2.data ?? {}), 25 | connect: userModel2.connect ?? userModel1.connect, 26 | }; 27 | }); 28 | 29 | userModels2Keys.forEach((modelName) => { 30 | if (!userModels1Keys.includes(modelName)) { 31 | mergedUserModels[modelName] = userModels2[modelName]; 32 | } 33 | }); 34 | 35 | return mergedUserModels; 36 | } 37 | 38 | const mergeUserModelsData = ( 39 | data1: UserModelsData, 40 | data2: UserModelsData, 41 | ): UserModelsData => { 42 | const results: UserModelsData = {}; 43 | 44 | const keys = dedupePreferLast([...Object.keys(data1), ...Object.keys(data2)]); 45 | 46 | for (const key of keys) { 47 | results[key] = Object.hasOwn(data2, key) ? data2[key] : data1[key]; 48 | } 49 | 50 | return results; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/seed/src/core/version.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from "node:fs"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { jsonStringify } from "./utils.js"; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | let version: string | undefined; 10 | 11 | export const writePkg = (data: Record) => { 12 | const productionPath = join(__dirname, "..", "..", "..", "package.json"); 13 | const testPath = join(__dirname, "..", "..", "package.json"); 14 | if (existsSync(productionPath)) { 15 | writeFileSync(productionPath, jsonStringify(data, undefined, 2)); 16 | return; 17 | } 18 | writeFileSync(testPath, jsonStringify(data, undefined, 2)); 19 | return; 20 | }; 21 | 22 | export const readPkg = () => { 23 | const productionPath = join(__dirname, "..", "..", "..", "package.json"); 24 | const testPath = join(__dirname, "..", "..", "package.json"); 25 | if (existsSync(productionPath)) { 26 | const content = readFileSync(productionPath); 27 | return JSON.parse(content.toString("utf-8")) as Result; 28 | } 29 | const content = readFileSync(testPath); 30 | return JSON.parse(content.toString("utf-8")) as Result; 31 | }; 32 | 33 | export const getVersion = () => 34 | (version ??= readPkg<{ version: string }>().version); 35 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/dialects.ts: -------------------------------------------------------------------------------- 1 | import { mysqlDialect } from "./mysql/dialect.js"; 2 | import { postgresDialect } from "./postgres/dialect.js"; 3 | import { sqliteDialect } from "./sqlite/dialect.js"; 4 | 5 | const dialects = { 6 | [mysqlDialect.id]: mysqlDialect, 7 | [postgresDialect.id]: postgresDialect, 8 | [sqliteDialect.id]: sqliteDialect, 9 | }; 10 | 11 | export type DialectId = keyof typeof dialects; 12 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/getDialect.ts: -------------------------------------------------------------------------------- 1 | import { getAdapter } from "#adapters/getAdapter.js"; 2 | import { mysqlDialect } from "./mysql/dialect.js"; 3 | import { postgresDialect } from "./postgres/dialect.js"; 4 | import { sqliteDialect } from "./sqlite/dialect.js"; 5 | 6 | async function getDialectId() { 7 | const adapter = await getAdapter(); 8 | 9 | return adapter.getDialect(); 10 | } 11 | 12 | export async function getDialect() { 13 | const dialectId = await getDialectId(); 14 | 15 | switch (dialectId) { 16 | case "postgres": 17 | return postgresDialect; 18 | case "sqlite": 19 | return sqliteDialect; 20 | case "mysql": 21 | return mysqlDialect; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/dataModel.ts: -------------------------------------------------------------------------------- 1 | import { type DataModel } from "#core/dataModel/types.js"; 2 | import { type DatabaseClient } from "#core/databaseClient.js"; 3 | import { introspectDatabase } from "./introspect/introspectDatabase.js"; 4 | import { introspectionToDataModel } from "./introspect/introspectionToDataModel.js"; 5 | 6 | export async function getDatamodel(_db: DatabaseClient): Promise { 7 | const introspection = await introspectDatabase(_db); 8 | const dataModel = introspectionToDataModel(introspection); 9 | return dataModel; 10 | } 11 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/determineShapeFromType.ts: -------------------------------------------------------------------------------- 1 | import { type DetermineShapeFromType } from "#core/dialect/types.js"; 2 | import { determineShapeFromType as _determineShapeFromType } from "#core/dialect/utils.js"; 3 | 4 | export const determineShapeFromType: DetermineShapeFromType = ( 5 | type: string, 6 | ) => { 7 | return _determineShapeFromType(type); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/dialect.ts: -------------------------------------------------------------------------------- 1 | import { generateConfigTypes } from "#core/dialect/generateConfigTypes.js"; 2 | import { type Dialect } from "#core/dialect/types.js"; 3 | import { getDatamodel } from "./dataModel.js"; 4 | import { determineShapeFromType } from "./determineShapeFromType.js"; 5 | import { generateClientTypes } from "./generateClientTypes.js"; 6 | import { SQL_TEMPLATES } from "./userModels.js"; 7 | 8 | export const mysqlDialect = { 9 | id: "mysql" as const, 10 | generateClientTypes, 11 | generateConfigTypes, 12 | determineShapeFromType, 13 | templates: SQL_TEMPLATES, 14 | getDataModel: getDatamodel, 15 | } satisfies Dialect; 16 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/generateClientTypes.ts: -------------------------------------------------------------------------------- 1 | import { generateClientTypes as _generateClientTypes } from "#core/codegen/generateClientTypes.js"; 2 | import { type DataModel } from "#core/dataModel/types.js"; 3 | import { type Fingerprint } from "#core/fingerprint/types.js"; 4 | import { 5 | SQL_DATE_TYPES, 6 | SQL_TO_JS_TYPES, 7 | extractPrimitiveSQLType, 8 | } from "./utils.js"; 9 | 10 | export function generateClientTypes(props: { 11 | dataModel: DataModel; 12 | fingerprint?: Fingerprint; 13 | }) { 14 | return _generateClientTypes({ 15 | ...props, 16 | database2tsType: mysql2tsType, 17 | isJson, 18 | refineType, 19 | }); 20 | } 21 | 22 | function mysql2tsType( 23 | dataModel: DataModel, 24 | sqliteType: string, 25 | isRequired: boolean, 26 | ) { 27 | const type = mysql2tsTypeName(dataModel, sqliteType); 28 | 29 | return refineType(type, sqliteType, isRequired); 30 | } 31 | 32 | function mysql2tsTypeName(dataModel: DataModel, sqliteType: string) { 33 | const primitiveType = extractPrimitiveSQLType(sqliteType); 34 | // In mysql, booleans are converted and stored as tinyint(1) 35 | // in this case, we want to allow the user to priovide a boolean or a number 36 | if (primitiveType === "tinyint") { 37 | return "( boolean | number )"; 38 | } 39 | if (SQL_DATE_TYPES.has(primitiveType)) { 40 | return "( Date | string )"; 41 | } 42 | const jsType = SQL_TO_JS_TYPES[primitiveType]; 43 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 44 | if (jsType) { 45 | return jsType; 46 | } 47 | 48 | const enumName = Object.keys(dataModel.enums).find( 49 | (name) => name === primitiveType, 50 | ); 51 | 52 | if (enumName) { 53 | return `${enumName}Enum`; 54 | } 55 | 56 | return "unknown"; 57 | } 58 | 59 | function refineType(type: string, _sqliteType: string, isRequired: boolean) { 60 | if (!isRequired) { 61 | type = `${type} | null`; 62 | } 63 | 64 | return type; 65 | } 66 | 67 | function isJson(databaseType: string) { 68 | return ["json", "jsonb"].includes(extractPrimitiveSQLType(databaseType)); 69 | } 70 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/introspect/queries/fetchEnums.ts: -------------------------------------------------------------------------------- 1 | import { snakeCase } from "change-case"; 2 | import { type DatabaseClient } from "#core/databaseClient.js"; 3 | import { buildSchemaInclusionClause } from "./utils.js"; 4 | 5 | interface FetchEnumsResult { 6 | id: string; 7 | schema: string; 8 | values: string; 9 | } 10 | 11 | const FETCH_ENUMS = (schemas: Array) => ` 12 | SELECT 13 | TABLE_SCHEMA AS \`schema\`, 14 | CONCAT('enum_', TABLE_SCHEMA, '_', TABLE_NAME, '_', COLUMN_NAME) AS id, 15 | SUBSTRING(COLUMN_TYPE FROM 6 FOR LENGTH(COLUMN_TYPE) - 6) AS \`values\` 16 | FROM information_schema.COLUMNS 17 | WHERE 18 | DATA_TYPE = 'enum' AND 19 | ${buildSchemaInclusionClause(schemas, "TABLE_SCHEMA")} 20 | ORDER BY CONCAT(TABLE_SCHEMA, '.', COLUMN_NAME); 21 | `; 22 | 23 | export async function fetchEnums( 24 | client: DatabaseClient, 25 | schemas: Array, // list of schemas related to the current database 26 | ) { 27 | const response = await client.query(FETCH_ENUMS(schemas)); 28 | return response.map((row) => ({ 29 | id: snakeCase(row.id), 30 | schema: row.schema, 31 | name: snakeCase(row.id), 32 | // Splitting the values string by comma, then trim spaces and remove single quotes 33 | values: row.values 34 | .split(",") 35 | .map((value) => value.trim().replace(/^'(.*)'$/, "$1")), 36 | })); 37 | } 38 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/introspect/queries/fetchSchemas.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | 3 | interface SchemaPair { 4 | CONSTRAINT_SCHEMA: string; 5 | REFERENCED_TABLE_SCHEMA: string; 6 | } 7 | 8 | const FETCH_ALL_SCHEMA_RELATIONSHIPS = ` 9 | SELECT DISTINCT 10 | CONSTRAINT_SCHEMA, 11 | REFERENCED_TABLE_SCHEMA 12 | FROM 13 | information_schema.KEY_COLUMN_USAGE 14 | WHERE 15 | REFERENCED_TABLE_SCHEMA IS NOT NULL 16 | AND TABLE_SCHEMA != REFERENCED_TABLE_SCHEMA; 17 | `; 18 | 19 | // Define a function to find all related schemas 20 | function findRelatedSchemas( 21 | startSchema: string, 22 | schemaPairs: Array, 23 | ) { 24 | let related = new Set([startSchema]); 25 | let newFound = true; 26 | 27 | while (newFound) { 28 | newFound = false; 29 | let currentRelated = new Set([...related]); // Snapshot of currently related schemas 30 | 31 | schemaPairs.forEach((pair) => { 32 | if ( 33 | currentRelated.has(pair.CONSTRAINT_SCHEMA) && 34 | !related.has(pair.REFERENCED_TABLE_SCHEMA) 35 | ) { 36 | related.add(pair.REFERENCED_TABLE_SCHEMA); 37 | newFound = true; 38 | } 39 | if ( 40 | currentRelated.has(pair.REFERENCED_TABLE_SCHEMA) && 41 | !related.has(pair.CONSTRAINT_SCHEMA) 42 | ) { 43 | related.add(pair.CONSTRAINT_SCHEMA); 44 | newFound = true; 45 | } 46 | }); 47 | } 48 | 49 | return [...related]; // Convert Set to Array 50 | } 51 | 52 | export async function fetchSchemas(client: DatabaseClient) { 53 | // Fetch all schema relationships 54 | const relationships = await client.query( 55 | FETCH_ALL_SCHEMA_RELATIONSHIPS, 56 | ); 57 | 58 | // Find the current database name 59 | const currentDatabase = await client.query<{ currentDatabase: string }>( 60 | "SELECT DATABASE() as currentDatabase", 61 | ); 62 | const currentDbName = currentDatabase[0].currentDatabase; 63 | // Use the function to find all schemas related to the current database 64 | const relatedSchemas = findRelatedSchemas(currentDbName, relationships); 65 | return relatedSchemas; 66 | } 67 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/introspect/queries/fetchSequences.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { 3 | buildSchemaInclusionClause, 4 | updateDatabasesTablesInfos, 5 | } from "./utils.js"; 6 | 7 | interface FetchSequencesResult { 8 | columnName: string; 9 | current: bigint | number; 10 | name: string; 11 | schema: string; 12 | } 13 | 14 | // Updated query to fetch AUTO_INCREMENT information along with the specific column 15 | const FETCH_SEQUENCES = (schemas: Array) => ` 16 | SELECT 17 | t.TABLE_SCHEMA AS \`schema\`, 18 | t.TABLE_NAME AS name, 19 | t.AUTO_INCREMENT AS current, 20 | c.COLUMN_NAME AS columnName 21 | FROM 22 | information_schema.TABLES t 23 | JOIN 24 | information_schema.COLUMNS c ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME 25 | WHERE 26 | t.TABLE_TYPE = 'BASE TABLE' AND 27 | t.AUTO_INCREMENT IS NOT NULL AND 28 | c.EXTRA LIKE '%auto_increment%' AND 29 | ${buildSchemaInclusionClause(schemas, "t.TABLE_SCHEMA")} 30 | ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME; 31 | `; 32 | 33 | export async function fetchSequences( 34 | client: DatabaseClient, 35 | schemas: Array, 36 | ) { 37 | // MySQL will delegate updating the informations_schemas infos by default 38 | // When fetching sequences, we need to make sure the tables infos are up-to-date 39 | await updateDatabasesTablesInfos(client, schemas); 40 | const response = await client.query( 41 | FETCH_SEQUENCES(schemas), 42 | ); 43 | 44 | return response.map((r) => ({ 45 | columnName: r.columnName, 46 | tableId: `${r.schema}.${r.name}`, 47 | schema: r.schema, 48 | name: `${r.schema}.${r.name}.${r.columnName}`, 49 | start: Number(1), // Auto-increment always starts from 1 in MySQL 50 | current: Number(r.current), // Adjusting to simulate 'last_value' from Mysql 51 | interval: Number(1), // Interval is always 1 in MySQL 52 | })); 53 | } 54 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/introspect/queries/fetchUniqueConstraints.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { buildSchemaInclusionClause } from "./utils.js"; 3 | 4 | interface FetchUniqueConstraintsResult { 5 | /** 6 | * The columns that are part of the constraint 7 | */ 8 | columns: Array; 9 | /** 10 | * allows us to always know if the constraint we are using 11 | * are the one retrieved from the database or the one we augmented via user setting or fallback 12 | */ 13 | dirty: boolean; 14 | /** 15 | * The constraint name 16 | */ 17 | name: string; 18 | /** 19 | * The schema name 20 | */ 21 | schema: string; 22 | /** 23 | * The table name 24 | */ 25 | table: string; 26 | /** 27 | * The table id (schemaName.tableName) 28 | */ 29 | tableId: string; 30 | } 31 | 32 | const FETCH_UNIQUE_CONSTRAINTS = (schemas: Array) => ` 33 | SELECT 34 | tc.CONSTRAINT_SCHEMA as \`schema\`, 35 | tc.TABLE_NAME as \`table\`, 36 | tc.CONSTRAINT_NAME as \`name\`, 37 | GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as \`columns\` 38 | FROM information_schema.TABLE_CONSTRAINTS tc 39 | JOIN information_schema.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME 40 | AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA 41 | AND tc.TABLE_NAME = kcu.TABLE_NAME 42 | WHERE (tc.CONSTRAINT_TYPE = 'UNIQUE' OR tc.CONSTRAINT_TYPE = 'PRIMARY KEY') AND ${buildSchemaInclusionClause(schemas, "tc.TABLE_SCHEMA")} 43 | GROUP BY tc.CONSTRAINT_SCHEMA, tc.TABLE_NAME, tc.CONSTRAINT_NAME 44 | `; 45 | 46 | export async function fetchUniqueConstraints( 47 | client: DatabaseClient, 48 | schemas: Array, 49 | ) { 50 | const response = await client.query<{ 51 | columns: string; 52 | name: string; 53 | schema: string; 54 | table: string; 55 | }>(FETCH_UNIQUE_CONSTRAINTS(schemas)); 56 | 57 | return response.map( 58 | (row) => 59 | ({ 60 | columns: row.columns.split(","), 61 | dirty: false, 62 | name: `${row.schema}.${row.table}.${row.columns.split(",").join("_")}`, 63 | schema: row.schema, 64 | table: row.table, 65 | tableId: `${row.schema}.${row.table}`, 66 | }) satisfies FetchUniqueConstraintsResult, 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/introspect/queries/utils.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { escapeIdentifier, escapeLiteral } from "../../utils.js"; 3 | 4 | function buildSchemaInclusionClause( 5 | schemas: Array, 6 | escapedColumn: string, 7 | ) { 8 | return schemas 9 | .map((s) => `${escapedColumn} = ${escapeLiteral(s)}`) 10 | .join(" OR "); 11 | } 12 | 13 | const FETCH_TABLES = (schema: string) => ` 14 | SELECT TABLE_NAME as tableName FROM information_schema.TABLES WHERE TABLE_SCHEMA = ${escapeLiteral(schema)} 15 | `; 16 | const ANALYZE_TABLE = (schema: string, table: string) => ` 17 | ANALYZE TABLE ${escapeIdentifier(schema)}.${escapeIdentifier(table)} 18 | `; 19 | const OPTIMIZE_TABLE = (schema: string, table: string) => ` 20 | OPTIMIZE TABLE ${escapeIdentifier(schema)}.${escapeIdentifier(table)} 21 | `; 22 | 23 | async function updateDatabasesTablesInfos( 24 | client: DatabaseClient, 25 | schemas: Array, 26 | ) { 27 | for (const schema of schemas) { 28 | const tables = await client.query<{ tableName: string }>( 29 | FETCH_TABLES(schema), 30 | ); 31 | for (const { tableName } of tables) { 32 | await client.execute(ANALYZE_TABLE(schema, tableName)); 33 | await client.execute(OPTIMIZE_TABLE(schema, tableName)); 34 | } 35 | } 36 | } 37 | 38 | export { buildSchemaInclusionClause, updateDatabasesTablesInfos }; 39 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/introspect/types.ts: -------------------------------------------------------------------------------- 1 | export type AsyncFunctionSuccessType< 2 | T extends (...args: never) => Promise, 3 | > = Awaited>; 4 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/userModels.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_SQL_TEMPLATES } from "#core/dialect/userModels.js"; 2 | import { type Templates } from "#core/userModels/templates/types.js"; 3 | import { 4 | type MYSQL_GEOMETRY_TYPES, 5 | geometryCollection, 6 | lineString, 7 | multiLineString, 8 | multiPoint, 9 | multiPolygon, 10 | point, 11 | polygon, 12 | } from "./userModels/templates/geometry.js"; 13 | import { type SQLTypeName } from "./utils.js"; 14 | 15 | export const SQL_TEMPLATES: Templates = { 16 | ...DEFAULT_SQL_TEMPLATES, 17 | point, 18 | geometry: point, 19 | linestring: lineString, 20 | polygon, 21 | multipoint: multiPoint, 22 | multilinestring: multiLineString, 23 | multipolygon: multiPolygon, 24 | geomcollection: geometryCollection, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/mysql/utils.ts: -------------------------------------------------------------------------------- 1 | import SqlString from "sqlstring"; 2 | import { type Serializable } from "#core/data/types.js"; 3 | import { SQL_DATE_TYPES } from "./utils.js"; 4 | 5 | export * from "#core/dialect/utils.js"; 6 | 7 | export const escapeIdentifier = (value: string | undefined) => 8 | SqlString.escapeId(value); 9 | export const escapeLiteral = SqlString.escape; 10 | 11 | const SQL_GEOMETRIC_TYPES = new Set([ 12 | "point", 13 | "geometry", 14 | "linestring", 15 | "polygon", 16 | "multipoint", 17 | "multilinestring", 18 | "multipolygon", 19 | "geomcollection", 20 | ]); 21 | 22 | export const serializeToSQL = (type: string, value: Serializable) => { 23 | if (value === null) { 24 | return "NULL"; 25 | } 26 | if (SQL_GEOMETRIC_TYPES.has(type)) { 27 | return `ST_GeomFromText(${escapeLiteral(value as string)})`; 28 | } 29 | if (SQL_DATE_TYPES.has(type)) { 30 | return escapeLiteral(new Date(value as string)); 31 | } 32 | return escapeLiteral(value); 33 | }; 34 | 35 | export const formatValues = (values: Array>) => { 36 | return values.map((row) => `(${row.map((v) => v).join(", ")})`).join(", "); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/dataModel.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { postgres } from "#test/postgres/postgres/index.js"; 3 | import { getDatamodel } from "./dataModel.js"; 4 | 5 | const adapters = { 6 | postgres: () => postgres, 7 | }; 8 | 9 | describe.concurrent.each(["postgres"] as const)( 10 | "getDataModel: %s", 11 | (adapter) => { 12 | const { createTestDb } = adapters[adapter](); 13 | 14 | test("array types", async () => { 15 | const structure = ` 16 | CREATE TABLE public."foo" (bar text[][]); 17 | `; 18 | const db = await createTestDb(structure); 19 | await db.client.execute(`VACUUM ANALYZE;`); 20 | const result = await getDatamodel(db.client); 21 | expect(result.models["foo"].fields[0].type).toEqual("text[][]"); 22 | }); 23 | }, 24 | ); 25 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/dataModel.ts: -------------------------------------------------------------------------------- 1 | import { type DataModel } from "#core/dataModel/types.js"; 2 | import { type DatabaseClient } from "#core/databaseClient.js"; 3 | import { introspectDatabase } from "./introspect/introspectDatabase.js"; 4 | import { introspectionToDataModel } from "./introspect/introspectionToDataModel.js"; 5 | 6 | export async function getDatamodel(_db: DatabaseClient): Promise { 7 | const introspection = await introspectDatabase(_db); 8 | const dataModel = introspectionToDataModel(introspection); 9 | return dataModel; 10 | } 11 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/determineShapeFromType.ts: -------------------------------------------------------------------------------- 1 | import { type DetermineShapeFromType } from "#core/dialect/types.js"; 2 | import { unpackNestedType } from "#core/dialect/unpackNestedType.js"; 3 | import { determineShapeFromType as _determineShapeFromType } from "#core/dialect/utils.js"; 4 | 5 | export const determineShapeFromType: DetermineShapeFromType = ( 6 | wrappedType: string, 7 | ) => { 8 | const [type] = unpackNestedType(wrappedType); 9 | 10 | return _determineShapeFromType(type); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/dialect.ts: -------------------------------------------------------------------------------- 1 | import { generateConfigTypes } from "#core/dialect/generateConfigTypes.js"; 2 | import { type Dialect } from "#core/dialect/types.js"; 3 | import { getDatamodel } from "./dataModel.js"; 4 | import { determineShapeFromType } from "./determineShapeFromType.js"; 5 | import { generateClientTypes } from "./generateClientTypes.js"; 6 | import { SQL_TEMPLATES } from "./userModels.js"; 7 | 8 | export const postgresDialect = { 9 | id: "postgres" as const, 10 | generateClientTypes, 11 | generateConfigTypes, 12 | determineShapeFromType, 13 | templates: SQL_TEMPLATES, 14 | getDataModel: getDatamodel, 15 | } satisfies Dialect; 16 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/generateClientTypes.ts: -------------------------------------------------------------------------------- 1 | import { generateClientTypes as _generateClientTypes } from "#core/codegen/generateClientTypes.js"; 2 | import { type DataModel } from "#core/dataModel/types.js"; 3 | import { type Fingerprint } from "#core/fingerprint/types.js"; 4 | import { 5 | SQL_DATE_TYPES, 6 | SQL_TO_JS_TYPES, 7 | extractPrimitiveSQLType, 8 | getPgTypeArrayDimensions, 9 | isNestedArrayPgType, 10 | } from "./utils.js"; 11 | 12 | export function generateClientTypes(props: { 13 | dataModel: DataModel; 14 | fingerprint?: Fingerprint; 15 | }) { 16 | return _generateClientTypes({ 17 | ...props, 18 | database2tsType: pg2tsType, 19 | isJson, 20 | refineType, 21 | }); 22 | } 23 | 24 | function pg2tsType( 25 | dataModel: DataModel, 26 | postgresType: string, 27 | isRequired: boolean, 28 | ) { 29 | const type = pg2tsTypeName(dataModel, postgresType); 30 | 31 | return refineType(type, postgresType, isRequired); 32 | } 33 | 34 | function pg2tsTypeName(dataModel: DataModel, postgresType: string) { 35 | const primitiveType = extractPrimitiveSQLType(postgresType); 36 | if (SQL_DATE_TYPES.has(primitiveType)) { 37 | return "( Date | string )"; 38 | } 39 | const jsType = SQL_TO_JS_TYPES[primitiveType]; 40 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 41 | if (jsType) { 42 | return jsType; 43 | } 44 | 45 | const enumName = Object.keys(dataModel.enums).find( 46 | (name) => name === primitiveType, 47 | ); 48 | 49 | if (enumName) { 50 | return `${enumName}Enum`; 51 | } 52 | 53 | return "unknown"; 54 | } 55 | 56 | function refineType(type: string, postgresType: string, isRequired: boolean) { 57 | if (isNestedArrayPgType(postgresType)) { 58 | type = `${type}${"[]".repeat(getPgTypeArrayDimensions(postgresType))}`; 59 | } 60 | 61 | if (!isRequired) { 62 | type = `${type} | null`; 63 | } 64 | 65 | return type; 66 | } 67 | 68 | function isJson(databaseType: string) { 69 | return ["json", "jsonb"].includes(extractPrimitiveSQLType(databaseType)); 70 | } 71 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/introspect/queries/fetchEnums.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { buildSchemaExclusionClause } from "./utils.js"; 3 | 4 | interface FetchEnumsResult { 5 | id: string; 6 | name: string; 7 | schema: string; 8 | values: Array; 9 | } 10 | 11 | const FETCH_ENUMS = ` 12 | WITH 13 | accessible_schemas AS ( 14 | SELECT 15 | schema_name 16 | FROM information_schema.schemata 17 | WHERE 18 | ${buildSchemaExclusionClause("schema_name")} 19 | ) 20 | SELECT 21 | pg_namespace.nspname AS schema, 22 | pg_type.typname AS name, 23 | concat(pg_namespace.nspname, '.', pg_type.typname) AS id, 24 | json_agg(pg_enum.enumlabel ORDER BY pg_enum.enumlabel) AS values 25 | FROM pg_type 26 | INNER JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid 27 | INNER JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace 28 | INNER JOIN accessible_schemas s ON s.schema_name = pg_namespace.nspname 29 | WHERE ${buildSchemaExclusionClause("pg_namespace.nspname")} 30 | GROUP BY pg_namespace.nspname, pg_type.typname 31 | ORDER BY concat(pg_namespace.nspname, '.', pg_type.typname) 32 | `; 33 | 34 | export async function fetchEnums(client: DatabaseClient) { 35 | const response = await client.query(FETCH_ENUMS); 36 | 37 | return response; 38 | } 39 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/introspect/queries/fetchSchemas.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { SeedPostgres } from "#adapters/postgres/index.js"; 3 | import { postgres } from "#test/postgres/postgres/index.js"; 4 | import { fetchSchemas } from "./fetchSchemas.js"; 5 | 6 | const adapters = { 7 | postgres: () => postgres, 8 | }; 9 | 10 | describe.concurrent.each(["postgres"] as const)( 11 | "fetchSchemas: %s", 12 | (adapter) => { 13 | const { createTestDb, createTestRole } = adapters[adapter](); 14 | test("should fetch only the public schema", async () => { 15 | const db = await createTestDb(); 16 | const schemas = await fetchSchemas(db.client); 17 | expect(schemas).toEqual(["public"]); 18 | }); 19 | test("should fetch all schemas where the user can read", async () => { 20 | const structure = ` 21 | CREATE SCHEMA other; 22 | CREATE SCHEMA private; 23 | `; 24 | const db = await createTestDb(structure); 25 | const testRole = await createTestRole(db.client.client); 26 | await db.client.execute( 27 | `REVOKE ALL PRIVILEGES ON SCHEMA private FROM "${testRole.name}"; 28 | GRANT ALL PRIVILEGES ON SCHEMA other TO "${testRole.name}";`, 29 | ); 30 | const schemas = await fetchSchemas(new SeedPostgres(testRole.client)); 31 | expect(schemas.length).toBe(2); 32 | expect(schemas).toEqual(expect.arrayContaining(["other", "public"])); 33 | }); 34 | }, 35 | ); 36 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/introspect/queries/fetchSchemas.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { buildSchemaExclusionClause } from "./utils.js"; 3 | 4 | interface FetchSchemasResult { 5 | schemaName: string; 6 | } 7 | const FETCH_AUTHORIZED_SCHEMAS = ` 8 | SELECT 9 | schema_name as "schemaName" 10 | FROM 11 | information_schema.schemata 12 | WHERE 13 | ${buildSchemaExclusionClause("schema_name")} AND 14 | pg_catalog.has_schema_privilege(current_user, schema_name, 'USAGE') 15 | ORDER BY schema_name 16 | `; 17 | 18 | export async function fetchSchemas(client: DatabaseClient) { 19 | const response = await client.query( 20 | FETCH_AUTHORIZED_SCHEMAS, 21 | ); 22 | 23 | return response.map((row) => row.schemaName); 24 | } 25 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/introspect/queries/fetchSequences.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { buildSchemaExclusionClause } from "./utils.js"; 3 | 4 | interface FetchSequencesResult { 5 | interval: bigint | number; 6 | last: null | string; 7 | name: string; 8 | schema: string; 9 | start: string; 10 | } 11 | 12 | const FETCH_SEQUENCES = ` 13 | SELECT 14 | schemaname AS schema, 15 | sequencename AS name, 16 | start_value AS start, 17 | last_value as last, 18 | increment_by AS interval 19 | FROM 20 | pg_sequences 21 | WHERE ${buildSchemaExclusionClause("pg_sequences.schemaname")} 22 | `; 23 | 24 | export async function fetchSequences(client: DatabaseClient) { 25 | const response = await client.query(FETCH_SEQUENCES); 26 | 27 | return response.map((r) => { 28 | return { 29 | schema: r.schema, 30 | name: r.name, 31 | start: Number(r.start), 32 | // When a sequence is created, the current value is the start value and is available for use 33 | // but when the sequence is used for the first time, the current values is the last used one not available for use 34 | // so we increment it by one to get the next available value instead 35 | current: r.last ? Number(r.last) + 1 : Number(r.start), 36 | interval: Number(r.interval), 37 | }; 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/introspect/queries/utils.ts: -------------------------------------------------------------------------------- 1 | import { escapeLiteral } from "../../utils.js"; 2 | 3 | const EXCLUDED_SCHEMAS = ["information_schema", "pg\\_%"]; 4 | 5 | function buildSchemaExclusionClause(escapedColumn: string) { 6 | return EXCLUDED_SCHEMAS.map( 7 | (s) => `${escapedColumn} NOT LIKE ${escapeLiteral(s)}`, 8 | ).join(" AND "); 9 | } 10 | 11 | export { buildSchemaExclusionClause }; 12 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/introspect/types.ts: -------------------------------------------------------------------------------- 1 | export type AsyncFunctionSuccessType< 2 | T extends (...args: never) => Promise, 3 | > = Awaited>; 4 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/postgres/userModels.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_SQL_TEMPLATES } from "#core/dialect/userModels.js"; 2 | import { type Templates } from "#core/userModels/templates/types.js"; 3 | import { type SQLTypeName } from "./utils.js"; 4 | 5 | export const SQL_TEMPLATES: Templates = { 6 | ...DEFAULT_SQL_TEMPLATES, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/dataModel.ts: -------------------------------------------------------------------------------- 1 | import { type DataModel } from "#core/dataModel/types.js"; 2 | import { type DatabaseClient } from "#core/databaseClient.js"; 3 | import { introspectDatabase } from "./introspect/introspectDatabase.js"; 4 | import { introspectionToDataModel } from "./introspect/introspectionToDataModel.js"; 5 | 6 | export async function getDatamodel(_db: DatabaseClient): Promise { 7 | const introspection = await introspectDatabase(_db); 8 | const dataModel = introspectionToDataModel(introspection); 9 | return dataModel; 10 | } 11 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/determineShapeFromType.ts: -------------------------------------------------------------------------------- 1 | import { type DetermineShapeFromType } from "#core/dialect/types.js"; 2 | import { determineShapeFromType as _determineShapeFromType } from "#core/dialect/utils.js"; 3 | 4 | export const determineShapeFromType: DetermineShapeFromType = ( 5 | type: string, 6 | ) => { 7 | return _determineShapeFromType(type); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/dialect.ts: -------------------------------------------------------------------------------- 1 | import { generateConfigTypes } from "#core/dialect/generateConfigTypes.js"; 2 | import { type Dialect } from "#core/dialect/types.js"; 3 | import { getDatamodel } from "./dataModel.js"; 4 | import { determineShapeFromType } from "./determineShapeFromType.js"; 5 | import { generateClientTypes } from "./generateClientTypes.js"; 6 | import { SQL_TEMPLATES } from "./userModels.js"; 7 | 8 | export const sqliteDialect = { 9 | id: "sqlite" as const, 10 | generateClientTypes, 11 | generateConfigTypes, 12 | determineShapeFromType, 13 | templates: SQL_TEMPLATES, 14 | getDataModel: getDatamodel, 15 | } satisfies Dialect; 16 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/generateClientTypes.ts: -------------------------------------------------------------------------------- 1 | import { generateClientTypes as _generateClientTypes } from "#core/codegen/generateClientTypes.js"; 2 | import { type DataModel } from "#core/dataModel/types.js"; 3 | import { type Fingerprint } from "#core/fingerprint/types.js"; 4 | import { 5 | SQL_DATE_TYPES, 6 | SQL_TO_JS_TYPES, 7 | extractPrimitiveSQLType, 8 | } from "./utils.js"; 9 | 10 | export function generateClientTypes(props: { 11 | dataModel: DataModel; 12 | fingerprint?: Fingerprint; 13 | }) { 14 | return _generateClientTypes({ 15 | ...props, 16 | database2tsType: sqlite2tsType, 17 | isJson: () => false, 18 | refineType, 19 | }); 20 | } 21 | 22 | function sqlite2tsType( 23 | dataModel: DataModel, 24 | sqliteType: string, 25 | isRequired: boolean, 26 | ) { 27 | const type = sqlite2tsTypeName(dataModel, sqliteType); 28 | 29 | return refineType(type, sqliteType, isRequired); 30 | } 31 | 32 | function sqlite2tsTypeName(_dataModel: DataModel, sqliteType: string) { 33 | const primitiveType = extractPrimitiveSQLType(sqliteType); 34 | // TODO: Find a way to automatically turn Date into number via `getTime()` 35 | // if the adapter used is `prisma` 36 | if (SQL_DATE_TYPES.has(primitiveType)) { 37 | return "( Date | string | number )"; 38 | } 39 | const jsType = SQL_TO_JS_TYPES[primitiveType]; 40 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 41 | if (jsType) { 42 | return jsType; 43 | } 44 | 45 | return "unknown"; 46 | } 47 | 48 | function refineType(type: string, _sqliteType: string, isRequired: boolean) { 49 | if (!isRequired) { 50 | type = `${type} | null`; 51 | } 52 | 53 | return type; 54 | } 55 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/introspect/queries/fetchSequences.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { betterSqlite3 } from "#test/sqlite/better-sqlite3/index.js"; 3 | import { fetchSequences } from "./fetchSequences.js"; 4 | 5 | const adapters = { 6 | betterSqlite3: () => betterSqlite3, 7 | }; 8 | 9 | describe.concurrent.each(["betterSqlite3"] as const)( 10 | "fetchSequences: %s", 11 | (adapter) => { 12 | const { createTestDb } = adapters[adapter](); 13 | 14 | test("should fetch primary key autoincrement sequence", async () => { 15 | const structure = ` 16 | CREATE TABLE students ( 17 | student_id INTEGER PRIMARY KEY AUTOINCREMENT, 18 | name VARCHAR(100) NOT NULL 19 | ); 20 | `; 21 | const { client } = await createTestDb(structure); 22 | const sequences = await fetchSequences(client); 23 | expect(sequences).toEqual([ 24 | { 25 | tableId: "students", 26 | colId: "student_id", 27 | name: "students_student_id_seq", // The exact name might differ; adjust as necessary 28 | current: 1, 29 | }, 30 | ]); 31 | }); 32 | 33 | test("should fetch rowid sequence on table wihtout primary key", async () => { 34 | const structure = ` 35 | CREATE TABLE students ( 36 | name VARCHAR(100) NOT NULL 37 | ); 38 | `; 39 | const { client } = await createTestDb(structure); 40 | const sequences = await fetchSequences(client); 41 | expect(sequences).toEqual( 42 | expect.arrayContaining([ 43 | { 44 | tableId: "students", 45 | colId: "rowid", 46 | name: "students_rowid_seq", // The exact name might differ; adjust as necessary 47 | current: 1, 48 | }, 49 | ]), 50 | ); 51 | 52 | await client.execute( 53 | `INSERT INTO students (name) VALUES ('John Doe'), ('Jane Smith');`, 54 | ); 55 | expect(await fetchSequences(client)).toEqual( 56 | expect.arrayContaining([ 57 | { 58 | tableId: "students", 59 | colId: "rowid", 60 | name: "students_rowid_seq", // The exact name might differ; adjust as necessary 61 | current: 3, 62 | }, 63 | ]), 64 | ); 65 | }); 66 | }, 67 | ); 68 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/introspect/queries/fetchSequences.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseClient } from "#core/databaseClient.js"; 2 | import { escapeIdentifier } from "#dialects/sqlite/utils.js"; 3 | import { 4 | FETCH_TABLE_COLUMNS_LIST, 5 | type FetchTableAndColumnsResultRaw, 6 | type SQLiteAffinity, 7 | mapCommonTypesToAffinity, 8 | } from "./fetchTablesAndColumns.js"; 9 | 10 | export interface FetchSequencesResult { 11 | colId: string; 12 | current: number; 13 | name: string; 14 | tableId: string; 15 | } 16 | 17 | export async function fetchSequences(client: DatabaseClient) { 18 | const results: Array = []; 19 | const tableColumnsInfos = await client.query( 20 | FETCH_TABLE_COLUMNS_LIST, 21 | ); 22 | const tableColumnsInfosGrouped = tableColumnsInfos.reduce< 23 | Record< 24 | string, 25 | Array<{ affinity: SQLiteAffinity } & FetchTableAndColumnsResultRaw> 26 | > 27 | >((acc, row) => { 28 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 29 | if (!acc[row.tableId]) { 30 | acc[row.tableId] = []; 31 | } 32 | acc[row.tableId].push({ 33 | ...row, 34 | affinity: mapCommonTypesToAffinity(row.colType, row.colNotNull === 0), 35 | }); 36 | return acc; 37 | }, {}); 38 | for (const tableId in tableColumnsInfosGrouped) { 39 | const tableColumns = tableColumnsInfosGrouped[tableId]; 40 | const tablePk = tableColumns.find((column) => column.colPk); 41 | // The table must have an autoincrement pk column or will have an implicit rowid column 42 | // used as a sequence 43 | const pkKey = 44 | tablePk && tablePk.affinity === "integer" ? tablePk.colName : "rowid"; 45 | const maxSeqRes = await client.query<{ 46 | // prisma adapter can return bigint as number, so we need to handle both 47 | currentSequenceValue: bigint | number; 48 | }>( 49 | `SELECT MAX(${escapeIdentifier(pkKey)}) + 1 as currentSequenceValue FROM ${escapeIdentifier(tableId)}`, 50 | ); 51 | const maxSeqNo = maxSeqRes[0]; 52 | results.push({ 53 | colId: pkKey, 54 | tableId, 55 | name: `${tableId}_${pkKey}_seq`, 56 | current: Number(maxSeqNo.currentSequenceValue) || 1, 57 | }); 58 | } 59 | return results; 60 | } 61 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/introspect/types.ts: -------------------------------------------------------------------------------- 1 | export type AsyncFunctionSuccessType< 2 | T extends (...args: never) => Promise, 3 | > = Awaited>; 4 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/userModels.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_SQL_TEMPLATES } from "#core/dialect/userModels.js"; 2 | import { type Templates } from "#core/userModels/templates/types.js"; 3 | import { type SQLTypeName } from "./utils.js"; 4 | 5 | export const SQL_TEMPLATES: Templates = { 6 | ...DEFAULT_SQL_TEMPLATES, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/seed/src/dialects/sqlite/utils.ts: -------------------------------------------------------------------------------- 1 | import { type Serializable } from "#core/data/types.js"; 2 | import { 3 | SQL_TO_JS_TYPES, 4 | extractPrimitiveSQLType, 5 | } from "#core/dialect/utils.js"; 6 | import { jsonStringify } from "#core/utils.js"; 7 | 8 | export * from "#core/dialect/utils.js"; 9 | 10 | export const serializeToSQL = (type: string, value: Serializable) => { 11 | const jsType = SQL_TO_JS_TYPES[extractPrimitiveSQLType(type)]; 12 | 13 | if (["json", "jsonb"].includes(type)) { 14 | return jsonStringify(value); 15 | } 16 | 17 | if (jsType === "boolean") { 18 | return value ? 1 : 0; 19 | } 20 | 21 | // SQLite does not automatically convert string to buffers when inserting into a BLOB column 22 | // we need to manually ensure that the value is converted to a buffer so that it can be inserted using X'' syntax 23 | if (jsType === "Buffer" && value && !(value instanceof Buffer)) { 24 | // eslint-disable-next-line @typescript-eslint/no-base-to-string 25 | return Buffer.from(value.toString()); 26 | } 27 | 28 | return value; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/seed/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../assets/index.js"; 2 | -------------------------------------------------------------------------------- /packages/seed/test/constants.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | 4 | const __filename = fileURLToPath(import.meta.url); 5 | const __dirname = path.dirname(__filename); 6 | 7 | export const ROOT_DIR = path.resolve(__dirname, ".."); 8 | 9 | // context(justinvdm, 11 Mar 2023): We put the temp dir in the project so that the temporary seed 10 | // scripts we create for each test are able to resolve the same modules in node_modules 11 | export const TMP_DIR = path.join(ROOT_DIR, ".tmp"); 12 | -------------------------------------------------------------------------------- /packages/seed/test/createDataModelFromSql.ts: -------------------------------------------------------------------------------- 1 | import { type DataModel } from "#core/dataModel/types.js"; 2 | import { type Adapter } from "./adapters.js"; 3 | 4 | export const createDataModelFromSql = async ( 5 | adapter: Adapter, 6 | sql: string, 7 | ): Promise => { 8 | const { client } = await adapter.createTestDb(sql); 9 | return adapter.dialect.getDataModel(client); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/seed/test/createTmpDirectory.ts: -------------------------------------------------------------------------------- 1 | import { type DirectoryResult, dir } from "tmp-promise"; 2 | 3 | interface State { 4 | tmpPaths: Array<{ 5 | dir: DirectoryResult; 6 | keep: boolean; 7 | name: string; 8 | }>; 9 | } 10 | 11 | const defineCreateTestTmpDirectory = (state: State) => { 12 | const createTestTmpDirectory = async ( 13 | keep = false, 14 | ): Promise => { 15 | const x = await dir({ tries: 10 }); 16 | const path = { keep, name: x.path, dir: x }; 17 | state.tmpPaths.push(path); 18 | return path; 19 | }; 20 | 21 | createTestTmpDirectory.afterAll = async () => { 22 | const tmpPaths = state.tmpPaths; 23 | state.tmpPaths = []; 24 | 25 | const cleanupFunctions = tmpPaths 26 | .filter((x) => !x.keep) 27 | .map((x) => x.dir.cleanup); 28 | try { 29 | await Promise.allSettled(cleanupFunctions); 30 | } catch (e) { 31 | console.error(e); 32 | } 33 | }; 34 | 35 | return createTestTmpDirectory; 36 | }; 37 | 38 | export const createTestTmpDirectory = defineCreateTestTmpDirectory({ 39 | tmpPaths: [], 40 | }); 41 | -------------------------------------------------------------------------------- /packages/seed/test/debug.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | 3 | export const testDebug = debug("snaplet").extend("seed").extend("test"); 4 | -------------------------------------------------------------------------------- /packages/seed/test/mysql/mysql/index.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "debug"; 2 | import { afterAll } from "vitest"; 3 | import { createSnapletTestDb, createTestDb } from "./createTestDatabase.js"; 4 | 5 | export const mysql = { 6 | createTestDb, 7 | createSnapletTestDb, 8 | }; 9 | 10 | const debugTest = debug("snaplet:test"); 11 | 12 | afterAll(async () => { 13 | await createTestDb.afterAll().catch((e: unknown) => { 14 | debugTest(e); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/seed/test/postgres/postgres/createTestRole.ts: -------------------------------------------------------------------------------- 1 | import postgres from "postgres"; 2 | import { v4 } from "uuid"; 3 | import { SeedPostgres } from "#adapters/postgres/index.js"; 4 | 5 | interface State { 6 | roles: Array<{ 7 | client: postgres.Sql; 8 | name: string; 9 | }>; 10 | } 11 | 12 | const TEST_DATABASE_SERVER = 13 | process.env["PG_TEST_DATABASE_SERVER"] ?? 14 | "postgres://postgres@127.0.0.1:5432/postgres"; 15 | const TEST_ROLE_PREFIX = "testrole"; 16 | 17 | const defineCreateTestRole = (state: State) => { 18 | const serverClient = postgres(TEST_DATABASE_SERVER, { max: 1 }); 19 | const serverDatabaseClient = new SeedPostgres(serverClient); 20 | const createTestRole = async (client: postgres.Sql) => { 21 | const roleName = `${TEST_ROLE_PREFIX}${v4()}`; 22 | await serverDatabaseClient.execute( 23 | `CREATE ROLE "${roleName}" WITH LOGIN PASSWORD 'password'`, 24 | ); 25 | const loggedClient = postgres(TEST_DATABASE_SERVER, { 26 | max: 1, 27 | database: client.options.database, 28 | username: roleName, 29 | password: "password", 30 | }); 31 | const result = { 32 | client: loggedClient, 33 | name: roleName, 34 | }; 35 | state.roles.push(result); 36 | return result; 37 | }; 38 | 39 | createTestRole.afterAll = async () => { 40 | const roles = state.roles; 41 | state.roles = []; 42 | 43 | const failures: Array<{ error: Error; roleName: string }> = []; 44 | 45 | // Close all pools connections on the database, if there is more than one to be able to drop it 46 | for (const { name } of roles) { 47 | try { 48 | await serverDatabaseClient.execute(`DROP ROLE IF EXISTS "${name}"`); 49 | } catch (error) { 50 | failures.push({ 51 | roleName: name, 52 | error: error as Error, 53 | }); 54 | } 55 | } 56 | 57 | if (failures.length) { 58 | throw new Error( 59 | [ 60 | "Failed to delete all roleNames, note that these will need to be manually cleaned up:", 61 | JSON.stringify(failures, null, 2), 62 | ].join("\n"), 63 | ); 64 | } 65 | }; 66 | 67 | return createTestRole; 68 | }; 69 | 70 | export const createTestRole = defineCreateTestRole({ roles: [] }); 71 | -------------------------------------------------------------------------------- /packages/seed/test/postgres/postgres/index.ts: -------------------------------------------------------------------------------- 1 | import { debug } from "debug"; 2 | import { afterAll } from "vitest"; 3 | import { createSnapletTestDb, createTestDb } from "./createTestDatabase.js"; 4 | import { createTestRole } from "./createTestRole.js"; 5 | 6 | export const postgres = { 7 | createTestDb, 8 | createSnapletTestDb, 9 | createTestRole, 10 | }; 11 | 12 | const debugTest = debug("snaplet:test"); 13 | 14 | afterAll(async () => { 15 | await createTestRole.afterAll().catch((e: unknown) => { 16 | debugTest(e); 17 | }); 18 | await createTestDb.afterAll().catch((e: unknown) => { 19 | debugTest(e); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/seed/test/runCli.ts: -------------------------------------------------------------------------------- 1 | import c from "ansi-colors"; 2 | import { execa } from "execa"; 3 | import path from "node:path"; 4 | import { inspect } from "node:util"; 5 | import { expect } from "vitest"; 6 | import { ROOT_DIR } from "./constants.js"; 7 | import { testDebug } from "./debug.js"; 8 | 9 | const debugCliRun = testDebug.extend("runCli"); 10 | const debugCliOutput = debugCliRun.extend("output"); 11 | // for the output we want to totally disable all prefix including namespace 12 | debugCliOutput.namespace = ""; 13 | 14 | const SHELL = "/bin/bash"; 15 | 16 | interface RunCliOptions { 17 | cwd?: string; 18 | env?: Partial; 19 | } 20 | 21 | export async function runCLI(args: Array, options: RunCliOptions = {}) { 22 | const { env: envOverrides = {}, cwd } = options; 23 | const entrypointTS = path.resolve(ROOT_DIR, "./src/cli/index.ts"); 24 | 25 | const { SNAPLET_ACCESS_TOKEN = "__test" } = process.env; 26 | 27 | const env = { 28 | SNAPLET_DISABLE_TELEMETRY: "1", 29 | NODE_ENV: "development", 30 | SNAPLET_ACCESS_TOKEN, 31 | SNAPLET_API_URL: "http://localhost:3000", 32 | ...envOverrides, 33 | }; 34 | 35 | debugCliRun( 36 | [ 37 | "", 38 | "==================================", 39 | c.bold("Running cli:"), 40 | `${c.bold("args:")} ${args.join(" ")}`, 41 | `${c.bold("test:")} ${expect.getState().currentTestName}`, 42 | Object.keys(envOverrides).length 43 | ? `${c.bold("env overrides:")} ${inspect(envOverrides)}` 44 | : null, 45 | "==================================", 46 | "", 47 | ] 48 | .filter((v) => v != null) 49 | .join("\n"), 50 | ); 51 | 52 | const result = execa( 53 | "tsx", 54 | ["--conditions=seed:development", entrypointTS, ...args], 55 | { 56 | shell: SHELL, 57 | stderr: "pipe", 58 | stdout: "pipe", 59 | cwd, 60 | preferLocal: true, 61 | env: { 62 | ...env, 63 | DEBUG_COLORS: "1", 64 | }, 65 | }, 66 | ); 67 | 68 | result.stdout?.on("data", (chunk: Buffer) => { 69 | debugCliOutput(chunk.toString().trim()); 70 | }); 71 | 72 | result.stderr?.on("data", (chunk: Buffer) => { 73 | debugCliOutput(chunk.toString().trim()); 74 | }); 75 | 76 | return result; 77 | } 78 | -------------------------------------------------------------------------------- /packages/seed/test/sqlite/better-sqlite3/createTestDatabase.ts: -------------------------------------------------------------------------------- 1 | import Database from "better-sqlite3"; 2 | import { copyFile } from "fs-extra"; 3 | import path from "node:path"; 4 | import { SeedBetterSqlite3 } from "#adapters/better-sqlite3/better-sqlite3.js"; 5 | import { type DatabaseClient } from "#core/databaseClient.js"; 6 | import { createTestTmpDirectory } from "../../createTmpDirectory.js"; 7 | 8 | const CHINOOK_DATABASE_PATH = path.resolve(__dirname, "../fixtures/chinook.db"); 9 | 10 | // We need this because we can't use better-sqlite3 .exec method for all the tests 11 | function splitSqlScript(script: string): Array { 12 | // Split on semicolon followed by optional whitespace and new line characters. 13 | // This simple approach might not correctly handle semicolons within string literals or comments. 14 | const queries = script.split(/;\s*(\n|$)/); 15 | 16 | // Filter out any empty strings that might result from the split (e.g., after the last semicolon) 17 | return queries.filter((query) => query.trim() !== ""); 18 | } 19 | 20 | export async function createTestDb(structure: string): Promise<{ 21 | client: DatabaseClient; 22 | connectionString: string; 23 | name: string; 24 | }> { 25 | const tmp = await createTestTmpDirectory(true); 26 | const connString = path.join(tmp.name, "test.sqlite3"); 27 | const db = new Database(connString); 28 | const client = new SeedBetterSqlite3(db); 29 | const queries = splitSqlScript(structure); 30 | for (const query of queries) { 31 | await client.execute(query); 32 | } 33 | return { 34 | client, 35 | name: connString, 36 | connectionString: `file://${connString}`, 37 | }; 38 | } 39 | 40 | // A sample database with data in it took from: https://www.sqlitetutorial.net/sqlite-sample-database/ 41 | export async function createChinookSqliteTestDatabase(): Promise<{ 42 | client: DatabaseClient; 43 | connectionString: string; 44 | name: string; 45 | }> { 46 | const tmp = await createTestTmpDirectory(); 47 | const connString = path.join(tmp.name, "chinook.sqlite3"); 48 | // copy chinook database to tmp directory 49 | await copyFile(CHINOOK_DATABASE_PATH, connString); 50 | const db = new Database(connString); 51 | const client = new SeedBetterSqlite3(db); 52 | return { 53 | client, 54 | name: connString, 55 | connectionString: connString, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/seed/test/sqlite/better-sqlite3/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createChinookSqliteTestDatabase, 3 | createTestDb, 4 | } from "./createTestDatabase.js"; 5 | 6 | export const betterSqlite3 = { 7 | createTestDb, 8 | createChinookSqliteTestDatabase, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/seed/test/sqlite/fixtures/chinook.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/seed/e109d00950185657f270093c0d7ad4d4ce4b26d3/packages/seed/test/sqlite/fixtures/chinook.db -------------------------------------------------------------------------------- /packages/seed/test/types.ts: -------------------------------------------------------------------------------- 1 | import { type DialectId } from "#dialects/dialects.js"; 2 | 3 | export type DialectRecordWithDefault = Partial> & 4 | Record<"default", string>; 5 | -------------------------------------------------------------------------------- /packages/seed/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@snaplet/tsconfig", 3 | "compilerOptions": { 4 | "rootDirs": ["src", "assets"], 5 | "outDir": "dist", 6 | "emitDeclarationOnly": false, 7 | "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", 8 | "paths": { 9 | "#*": ["./src/*"], 10 | } 11 | }, 12 | "include": [ 13 | "src", 14 | "assets/index.ts", 15 | "global.d.ts" 16 | ], 17 | "exclude": [ 18 | "src/**/*.test.ts", 19 | ], 20 | } -------------------------------------------------------------------------------- /packages/seed/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@snaplet/tsconfig", 3 | "compilerOptions": { 4 | "outDir": ".dts", 5 | }, 6 | "exclude": [ 7 | "**/dist/*", 8 | "**/.tmp/*", 9 | "e2e/fixtures/install/**/*", 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/seed/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import { dirname, join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import { defineProject } from "vitest/config"; 5 | 6 | const root = dirname(fileURLToPath(import.meta.url)); 7 | 8 | const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8")) as { 9 | name: string; 10 | }; 11 | 12 | export default defineProject({ 13 | test: { 14 | retry: process.env["CI"] ? 1 : 0, // Retry failed tests once in CI in case of flakiness due to parrallelization 15 | name: pkg.name, 16 | root, 17 | testTimeout: 120_000, 18 | sequence: { 19 | shuffle: false, 20 | }, 21 | setupFiles: ["dotenv/config"], 22 | }, 23 | esbuild: { 24 | target: "es2022", 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@snaplet/tsconfig", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "exports": { 6 | ".": "./tsconfig.json" 7 | }, 8 | "dependencies": { 9 | "@tsconfig/node20": "20.1.4", 10 | "@tsconfig/strictest": "2.0.5" 11 | } 12 | } -------------------------------------------------------------------------------- /packages/tsconfig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tsconfig/strictest", 4 | "@tsconfig/node20" 5 | ], 6 | "compilerOptions": { 7 | "composite": true, 8 | "customConditions": [ 9 | "seed:development" 10 | ], 11 | "strictNullChecks": true, 12 | "emitDeclarationOnly": true, 13 | "exactOptionalPropertyTypes": false, 14 | "noUncheckedIndexedAccess": false, 15 | "noEmit": false, 16 | }, 17 | "exclude": [ 18 | "**/dist/*" 19 | ] 20 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'docs' -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@snaplet/tsconfig", 3 | "compilerOptions": { 4 | "outDir": ".dts", 5 | }, 6 | "references": [ 7 | { 8 | "path": "./packages/seed" 9 | }, 10 | ], 11 | "exclude": [ 12 | "./packages", 13 | ] 14 | } -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": [ 6 | "^build" 7 | ], 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | }, 12 | "clean": { 13 | "cache": false 14 | }, 15 | "dev": { 16 | "cache": false, 17 | "persistent": true 18 | }, 19 | "lint": {}, 20 | "test": { 21 | "dependsOn": ["build"] 22 | }, 23 | "test:e2e": {}, 24 | "type-check": {} 25 | } 26 | } -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from "vitest/config"; 2 | 3 | export default defineWorkspace(["packages/*"]); 4 | --------------------------------------------------------------------------------