@podosoft/podokit 1.1.2 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,11 +7,38 @@ through native `Bun.SQL`, Tailwind CSS v4, shadcn-svelte, Docker, and k3s assets
7
7
  The CLI itself remains Node-compatible, so either `npx` or `bunx` can create and
8
8
  maintain projects. Generated PodoKit v1 applications are Bun-only.
9
9
 
10
+ ## Choose infrastructure
11
+
12
+ `fullstack` is the default template. `todo` adds a working CRUD example; `base`
13
+ is a minimal workspace with placeholder scripts. For a small working app, use
14
+ `fullstack` or `todo` with local providers rather than choosing `base` just to
15
+ avoid external infrastructure.
16
+
17
+ | Capability | Default | Local alternative |
18
+ | --- | --- | --- |
19
+ | Database | `postgres` — PostgreSQL | `sqlite` — database file |
20
+ | Cache | `redis` | `memory` — in-process cache |
21
+ | Object storage | `s3` — AWS S3 or S3-compatible storage | `local` — local files |
22
+ | Events | `redis` | `memory` — in-process events |
23
+ | Jobs | `bullmq` — Redis queue and separate worker | `local` — database-backed embedded worker |
24
+
25
+ A simple program, prototype, desktop app, or single-process service can use
26
+ **SQLite, memory cache, and local files** without PostgreSQL, Redis, or S3.
27
+ Providers can be mixed. Local providers require one API process.
28
+
29
+ Only the database is selected during creation with `--database postgres|sqlite`.
30
+ Other capabilities use `provider set` after creation; there are no `--cache` or
31
+ `--storage` creation flags. Choosing SQLite alone leaves the other defaults
32
+ unchanged. Creation records selections, while applying a provider or adding a
33
+ feature installs the necessary optional implementation modules.
34
+
10
35
  ## Quick start
11
36
 
37
+ ### Default server setup
38
+
12
39
  ```bash
13
40
  npx @podosoft/podokit create my-app
14
- # or: bunx @podosoft/podokit create my-app
41
+ # or: bunx --bun @podosoft/podokit create my-app
15
42
 
16
43
  cd my-app
17
44
  bun install
@@ -28,6 +55,26 @@ PostgreSQL or SQLite persistence:
28
55
 
29
56
  ![Generated todo app](https://raw.githubusercontent.com/podosoft-dev/podokit/main/docs/images/todo-app.png)
30
57
 
58
+ ### Small app with local providers
59
+
60
+ ```bash
61
+ bunx --bun @podosoft/podokit create podokit --template todo --database sqlite --yes
62
+ cd podokit
63
+ bunx --bun @podosoft/podokit provider set cache memory --apply
64
+ bunx --bun @podosoft/podokit provider set object-storage local --apply
65
+ bunx --bun @podosoft/podokit provider set events memory --apply
66
+ bunx --bun @podosoft/podokit provider set jobs local --apply
67
+ bun install
68
+ cp .env.example .env
69
+ bun run --cwd apps/api migration:run
70
+ ```
71
+
72
+ Run `bun run --cwd apps/api dev` and `bun run --cwd apps/web dev` in separate
73
+ terminals and open `http://localhost:5001`. This host-process setup requires no
74
+ Docker. SQLite and local files default to `./data/podokit.sqlite` and
75
+ `./data/files`, relative to the API working directory. Memory state disappears
76
+ on restart; files and the database-backed local jobs persist.
77
+
31
78
  ## Commands
32
79
 
33
80
  | Command | Purpose |
@@ -85,14 +132,21 @@ and [updating guide](https://github.com/podosoft-dev/podokit/blob/main/docs/upda
85
132
 
86
133
  ## Runtime providers
87
134
 
88
- Generated projects use stable capability contracts with server defaults
89
- (`postgres`, `redis`, `s3`, Redis events, and `bullmq`) and local alternatives
90
- (`sqlite`, memory cache/events, local files/jobs). `podo provider set` is a dry
91
- run by default and installs the selected implementation module on `--apply`.
92
- It changes code and configuration only; it never migrates or deletes data.
135
+ Inspect active selections and available alternatives with `podo provider list`.
136
+ `podo provider set` is a dry run by default and installs the selected
137
+ implementation module on `--apply`. Configure providers before adding features:
138
+ `file-upload` uses local storage when selected, and `job-progress` can use local
139
+ jobs and memory events. Changes append settings to `.env.example`; merge them
140
+ into an existing `.env` manually. Switching changes code and configuration only;
141
+ it never migrates or deletes data.
142
+
143
+ S3's development settings use Silo with `STORAGE_PROVIDER=minio`; use
144
+ `STORAGE_PROVIDER=aws` for AWS S3. Local files use `LOCAL_STORAGE_PATH`.
93
145
 
94
146
  See the [runtime provider guide](https://github.com/podosoft-dev/podokit/blob/main/docs/providers.md)
95
147
  before switching an existing project.
148
+ For the complete creation sequence, see
149
+ [Getting Started](https://github.com/podosoft-dev/podokit/blob/main/docs/getting-started.md).
96
150
 
97
151
  ## Compatibility boundary
98
152
 
@@ -5,19 +5,25 @@ by [PodoKit](https://github.com/podosoft-dev/podokit).
5
5
 
6
6
  ## Project overview
7
7
 
8
- A minimal Bun 1.4 workspace starter. Build it up from here — add features with the
9
- `podo` CLI (`podo add <module>`; run `podo add` with no argument to list them).
8
+ A minimal Bun 1.4 workspace starter with placeholder API/web scripts. Build your
9
+ own foundation here. Modules requiring the fullstack service registry, provider
10
+ consumers, or injection points cannot be added directly to this skeleton.
11
+
12
+ For a simple working full-stack app, create `fullstack` or `todo` with SQLite,
13
+ memory cache/events, local files, and local jobs. Those templates support local
14
+ providers independently of template choice and need one API process. See
15
+ [Getting Started](https://github.com/podosoft-dev/podokit/blob/main/docs/getting-started.md).
10
16
 
11
17
  ## Commands
12
18
 
13
19
  ```bash
14
20
  {{packageManager}} install
15
- {{rootRun}} dev # runs workspace dev scripts
16
21
  {{rootRun}} build
17
22
  {{rootRun}} lint # type-check
18
23
  {{rootRun}} test
19
24
  ```
20
25
 
26
+ The generated scripts are placeholders; implement them before running the app.
21
27
  ## Code style
22
28
 
23
29
  - TypeScript `strict`. **No `any`** (use `unknown` + narrowing), no `@ts-ignore`.
@@ -2,18 +2,22 @@
2
2
 
3
3
  Generated with [PodoKit](https://github.com/podosoft-dev/podokit).
4
4
 
5
- A minimal Bun 1.4 TypeScript workspace. Add the `fullstack` template when you
6
- need an Elysia API and SvelteKit web application.
5
+ A minimal Bun 1.4 TypeScript workspace with placeholder API/web scripts. Choose
6
+ `fullstack` or `todo` when creating a project that needs the working Elysia API,
7
+ SvelteKit web app, database integration, or feature modules.
8
+
9
+ For a simple working app, those templates support SQLite, memory cache/events,
10
+ local files, and local jobs without external PostgreSQL, Redis, or S3 services.
11
+ `base` is for building your own foundation; it does not include the provider
12
+ consumers or module integration points. See
13
+ [Getting Started](https://github.com/podosoft-dev/podokit/blob/main/docs/getting-started.md).
7
14
 
8
15
  ## Getting started
9
16
 
10
17
  ```bash
11
18
  {{packageManager}} install
12
- {{packageManager}} run dev
13
- ```
14
-
15
- Copy the example environment file before running:
16
-
17
- ```bash
18
19
  cp .env.example .env
19
20
  ```
21
+
22
+ Implement the workspace scripts before running the application. The generated
23
+ `dev` commands are placeholders and do not start services.
@@ -16,18 +16,36 @@ A Bun 1.4 full-stack TypeScript monorepo ({{packageManager}} workspaces):
16
16
 
17
17
  ## Setup & commands
18
18
 
19
+ Inspect active infrastructure with `podo provider list`. New-project defaults
20
+ are `database=postgres`, `cache=redis`, `object-storage=s3`, `events=redis`, and
21
+ `jobs=bullmq`. A simple single-process app can instead use `sqlite`, `memory`
22
+ cache/events, `local` files, and `local` jobs. Only the database has a creation
23
+ flag (`--database`); apply other choices using
24
+ `podo provider set <capability> <provider> --apply` before adding features.
25
+ Provider selection does not migrate or delete existing data.
26
+
19
27
  ```bash
20
28
  {{packageManager}} install
21
29
  cp .env.example .env
22
- docker compose -f infra/docker/docker-compose.yml up -d # selected external providers only
30
+ {{apiRun}} migration:run
23
31
  {{rootRun}} dev # api on http://localhost:5002, web on http://localhost:5001
24
32
  ```
25
33
 
34
+ For PostgreSQL, start `docker compose -f infra/docker/docker-compose.yml up -d`
35
+ before migrating. Add `--profile cache` for Redis; Silo uses the overlay from
36
+ `object-storage-s3`. Skip external services entirely for the complete local
37
+ combination. `podo dev watch` is a separate container workflow and requires
38
+ Docker. SQLite and local files default to `./data/podokit.sqlite` and
39
+ `./data/files` relative to the API working directory; migrations and runtime
40
+ must use the same database path. Keep local providers at one API process.
41
+ Merge added module settings from `.env.example` into an existing `.env`.
42
+
26
43
  - **Ports: web = 5001, api = 5002.** The web app reaches the API only through its
27
44
  SvelteKit server proxy (`/api/*`), never with a direct browser `fetch`.
28
45
  - Build: `{{rootRun}} build` · Type-check/lint: `{{rootRun}} lint` · Unit tests: `{{rootRun}} test`
29
46
  - Per workspace: `{{apiRun}} build` / `{{webRun}} build`.
30
- - DB migrations (TypeORM migration layer only): `{{apiRun}} migration:run`.
47
+ - Application migrations (PostgreSQL or SQLite): `{{apiRun}} migration:run`.
48
+ - When `auth` is installed, run `{{apiRun}} migrate:all` for Better Auth and application tables.
31
49
  - e2e (Playwright, ships in `tests/`): `{{rootRun}} test:e2e`.
32
50
  - Playwright officially runs on Node, so `bunx playwright` respects its Node
33
51
  shebang. Node LTS is a test-tool dependency only; API, web, worker, migrations,
@@ -2,9 +2,47 @@
2
2
 
3
3
  Full-stack TypeScript starter generated with [PodoKit](https://github.com/podosoft-dev/podokit).
4
4
 
5
- - `apps/api` — Bun 1.4 + Elysia API: schema-validated env, `/health` + `/health/ready`, merged OpenAPI at `/api-docs`, a standard error envelope, and Bun.SQL + PostgreSQL. TypeORM is used only to execute migrations.
5
+ - `apps/api` — Bun 1.4 + Elysia API: schema-validated env, `/health` + `/health/ready`, merged OpenAPI at `/api-docs`, a standard error envelope, and Bun.SQL with PostgreSQL or SQLite. TypeORM is used only for PostgreSQL migrations; SQLite has its own migration runner.
6
6
  - `apps/web` — SvelteKit app (TailwindCSS v4, shadcn-svelte, typesafe-i18n) that talks to the API through a server-side proxy.
7
- - `infra/` — Docker Compose (PostgreSQL, Redis) and k3s manifests.
7
+ - `infra/` — Docker Compose references for external services and k3s manifests.
8
+
9
+ ## Infrastructure choices
10
+
11
+ Inspect the current selections with `{{packageExecutor}} @podosoft/podokit provider list`.
12
+ New projects use these defaults unless configured otherwise:
13
+
14
+ | Capability | Default | Lightweight alternative |
15
+ | --- | --- | --- |
16
+ | Database | `postgres` | `sqlite` |
17
+ | Cache | `redis` | `memory` |
18
+ | Object storage | `s3` | `local` |
19
+ | Events | `redis` | `memory` |
20
+ | Jobs | `bullmq` | `local` |
21
+
22
+ For a simple program, prototype, or desktop app, SQLite, memory cache/events,
23
+ local files, and local jobs can run inside one API process without PostgreSQL,
24
+ Redis, or S3 services. `create --database sqlite` selects only the database;
25
+ choose the other providers after creation:
26
+
27
+ ```bash
28
+ {{packageExecutor}} @podosoft/podokit provider set cache memory --apply
29
+ {{packageExecutor}} @podosoft/podokit provider set object-storage local --apply
30
+ {{packageExecutor}} @podosoft/podokit provider set events memory --apply
31
+ {{packageExecutor}} @podosoft/podokit provider set jobs local --apply
32
+ ```
33
+
34
+ Apply providers before adding features, then install dependencies and merge new
35
+ `.env.example` entries into `.env`. These commands install implementation modules
36
+ and configure them; they do not move or delete data. SQLite defaults to
37
+ `./data/podokit.sqlite`, and local files to `LOCAL_STORAGE_PATH=./data/files`,
38
+ relative to the API working directory. Memory cache/events are lost on restart;
39
+ the database, files, and local job records persist. Use absolute paths and back
40
+ up the database and files together for packaged or production apps.
41
+
42
+ Use the host-process setup below to avoid Docker. The containerized loop still
43
+ requires Docker. See the PodoKit
44
+ [runtime provider guide](https://github.com/podosoft-dev/podokit/blob/main/docs/providers.md)
45
+ for server settings, Silo/AWS S3 configuration, and provider switching.
8
46
 
9
47
  ## Getting started
10
48
 
@@ -57,13 +95,27 @@ through Compose Watch, including Vite HMR on the same portless browser origin.
57
95
 
58
96
  ### Alternative: host processes
59
97
 
60
- Use this loop when you want only the web and API processes on the host:
98
+ Use this loop for an app with SQLite and local providers, or when you want the
99
+ web and API processes on the host. Prepare the project first:
100
+
101
+ ```bash
102
+ {{packageManager}} install
103
+ cp .env.example .env
104
+ {{apiRun}} migration:run
105
+ ```
106
+
107
+ For PostgreSQL, start `docker compose -f infra/docker/docker-compose.yml up -d`
108
+ before migrating. Redis needs `--profile cache`; Silo needs the storage overlay
109
+ added by `object-storage-s3`. Skip those services for the complete local setup.
110
+
111
+ Run each service in a separate terminal from the project root:
61
112
 
62
113
  ```bash
63
- # start local PostgreSQL + Redis
64
- docker compose -f infra/docker/docker-compose.yml up -d
114
+ {{apiRun}} dev # terminal 1
115
+ ```
65
116
 
66
- {{packageManager}} run dev
117
+ ```bash
118
+ {{webRun}} dev # terminal 2
67
119
  ```
68
120
 
69
121
  - API: http://localhost:5002 — health at `/health`, docs at `/api-docs`
@@ -71,13 +123,18 @@ docker compose -f infra/docker/docker-compose.yml up -d
71
123
 
72
124
  ## Database & migrations
73
125
 
74
- The API uses Bun.SQL for request-time queries and TypeORM only as a migration
75
- runner. Add a numbered migration under `apps/api/src/migrations`, then run it:
126
+ The API uses Bun.SQL with the selected PostgreSQL or SQLite database. TypeORM
127
+ runs PostgreSQL migrations; SQLite uses the local migration runner. Add a
128
+ numbered migration under `apps/api/src/migrations`, then run it:
76
129
 
77
130
  ```bash
78
131
  {{apiRun}} migration:run
79
132
  ```
80
133
 
134
+ When `auth` is installed, use `{{apiRun}} migrate:all` to create or update both
135
+ Better Auth and application tables. Set the module's required environment
136
+ values before migrating.
137
+
81
138
  Want a worked example? Generate the `todo` template instead:
82
139
  `{{packageExecutor}} @podosoft/podokit create my-app --template todo`.
83
140
 
@@ -6,7 +6,7 @@
6
6
  "dev": "bun --watch src/main.ts",
7
7
  "build": "bun scripts/build.mjs",
8
8
  "start": "bun dist/main.js",
9
- "lint": "bunx tsc -p tsconfig.json --noEmit",
9
+ "lint": "bunx --bun tsc -p tsconfig.json --noEmit",
10
10
  "test": "bun test src",
11
11
  "contract": "bun scripts/verify-api-contract.ts",
12
12
  "migrate:all": "bun dist/database/migrate.js",
@@ -1,170 +1,88 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { SUPPORTED_SOCIAL_PROVIDERS } from "@podosoft/podokit-auth";
2
+ import { Database } from "bun:sqlite";
3
3
  import {
4
- legacyAccountIssuer,
5
- legacyAccountIssuerTriggerSql,
6
4
  migrateLegacyAccountIssuers,
5
+ migrateSqliteLegacyAccountIssuers,
7
6
  type AccountIssuerMigrationClient,
8
- type AccountIssuerMigrationDatabase,
9
7
  } from "./account-issuer-migration";
10
8
 
11
- type Statement = { sql: string; values?: readonly unknown[] };
12
- type LegacyAccountOptions = {
13
- tableExists?: boolean;
14
- column?: { dataType: string; isNullable: "YES" | "NO" };
15
- counts?: {
16
- blankIssuer?: number;
17
- credentialMismatch?: number;
18
- unresolvedIssuer?: number;
19
- collision?: number;
20
- };
21
- };
22
-
23
- class LegacyAccountClient implements AccountIssuerMigrationClient {
24
- readonly statements: Statement[] = [];
9
+ class AccountClient implements AccountIssuerMigrationClient {
10
+ readonly statements: string[] = [];
25
11
  released = false;
26
-
27
- constructor(
28
- private readonly providers: readonly string[],
29
- private readonly options: LegacyAccountOptions = {},
30
- ) {}
31
-
32
- async query<Row extends Record<string, unknown>>(
33
- sql: string,
34
- values?: readonly unknown[],
35
- ): Promise<readonly Row[]> {
36
- this.statements.push({ sql, ...(values ? { values } : {}) });
37
- if (sql.includes("information_schema.tables")) {
38
- return [{ exists: this.options.tableExists ?? true }] as unknown as Row[];
39
- }
40
- if (sql.includes("information_schema.columns")) {
41
- return (this.options.column ? [this.options.column] : []) as unknown as Row[];
42
- }
43
- if (sql.includes('SELECT DISTINCT "providerId"')) {
44
- return this.providers.map((providerId) => ({ providerId })) as unknown as Row[];
45
- }
46
- if (sql.includes('COUNT(*)::int AS "count"')) {
47
- const count = sql.includes('"accountId" <> "userId"')
48
- ? this.options.counts?.credentialMismatch
49
- : sql.includes("AS collisions")
50
- ? this.options.counts?.collision
51
- : sql.includes('"issuer" IS NULL OR')
52
- ? this.options.counts?.unresolvedIssuer
53
- : this.options.counts?.blankIssuer;
54
- return [{ count: count ?? 0 }] as unknown as Row[];
55
- }
12
+ constructor(private readonly table = true, private readonly issuer = true, private readonly collisions = 0) {}
13
+ async query<Row extends Record<string, unknown>>(sql: string): Promise<readonly Row[]> {
14
+ this.statements.push(sql.trim());
15
+ if (sql.includes("to_regclass")) return [{ exists: this.table }] as unknown as Row[];
16
+ if (sql.includes("pg_attribute")) return [{ exists: this.issuer }] as unknown as Row[];
17
+ if (sql.includes("AS collisions")) return [{ count: this.collisions }] as unknown as Row[];
56
18
  return [];
57
19
  }
58
-
59
- release(): void {
60
- this.released = true;
61
- }
20
+ release(): void { this.released = true; }
62
21
  }
63
22
 
64
- function database(client: LegacyAccountClient): AccountIssuerMigrationDatabase {
65
- return { connect: async (): Promise<AccountIssuerMigrationClient> => client };
23
+ async function migrate(client: AccountClient): Promise<string> {
24
+ return migrateLegacyAccountIssuers({ connect: async (): Promise<AccountIssuerMigrationClient> => client });
66
25
  }
67
26
 
68
- describe("Better Auth account issuer migration", () => {
69
- it("maps every safely derivable provider and refuses identities that need trusted external data", () => {
70
- expect(legacyAccountIssuer("credential")).toBe("local:credential");
71
- expect(legacyAccountIssuer("google")).toBe("https://accounts.google.com");
72
- expect(legacyAccountIssuer("apple")).toBe("https://appleid.apple.com");
73
- expect(legacyAccountIssuer("facebook")).toBe("https://www.facebook.com");
74
- expect(legacyAccountIssuer("line")).toBe("https://access.line.me");
75
- expect(legacyAccountIssuer("github")).toBe("local:oauth:github");
76
- expect(legacyAccountIssuer("microsoft")).toBeUndefined();
77
- expect(legacyAccountIssuer("unknown-provider")).toBeUndefined();
78
-
79
- for (const { id } of SUPPORTED_SOCIAL_PROVIDERS) {
80
- if (id !== "microsoft") expect(legacyAccountIssuer(id), id).toBeDefined();
81
- }
82
- });
27
+ function legacyDatabase(): Database {
28
+ const database = new Database(":memory:");
29
+ database.run('CREATE TABLE account (id text PRIMARY KEY, "providerId" text NOT NULL, "accountId" text NOT NULL, "userId" text NOT NULL, password text, issuer text NOT NULL)');
30
+ database.run('CREATE UNIQUE INDEX "account_issuer_accountId_uidx" ON account (issuer, "accountId")');
31
+ database.run('INSERT INTO account VALUES (\'account\', \'credential\', \'user\', \'user\', \'password-hash\', \'local:credential\')');
32
+ return database;
33
+ }
83
34
 
84
- it("installs a fail-closed compatibility trigger for the release that is still serving", () => {
85
- const sql = legacyAccountIssuerTriggerSql();
86
- expect(sql).toContain("BEFORE INSERT");
87
- expect(sql).toContain('WHEN (NEW."issuer" IS NULL)');
88
- expect(sql).toContain("NEW.\"accountId\" := NEW.\"userId\"");
89
- expect(sql).toContain("RAISE EXCEPTION");
90
- expect(sql).not.toContain("WHEN 'microsoft'");
35
+ describe("Better Auth account identity cleanup", () => {
36
+ it("leaves an absent PostgreSQL table to the auth migrator", async (): Promise<void> => {
37
+ const client = new AccountClient(false);
38
+ expect(await migrate(client)).toBe("absent");
39
+ expect(client.statements.at(-1)).toBe("COMMIT");
40
+ expect(client.released).toBe(true);
91
41
  });
92
-
93
- it("adds, backfills, validates, constrains, and indexes a legacy account table in one transaction", async () => {
94
- const client = new LegacyAccountClient(["credential", "google", "github"]);
95
-
96
- await expect(migrateLegacyAccountIssuers(database(client))).resolves.toBe("migrated");
97
-
98
- const statements = client.statements.map(({ sql }) => sql.trim());
99
- expect(statements[0]).toBe("BEGIN");
100
- expect(statements.some((sql) => sql.includes('ADD COLUMN "issuer" text'))).toBe(true);
101
- expect(
102
- client.statements.some(
103
- ({ sql, values }) => sql.includes('UPDATE "account"') && values?.[0] === "local:credential",
104
- ),
105
- ).toBe(true);
106
- expect(
107
- client.statements.some(
108
- ({ sql, values }) => sql.includes('UPDATE "account"') && values?.[0] === "https://accounts.google.com",
109
- ),
110
- ).toBe(true);
111
- expect(statements.some((sql) => sql.includes("CREATE TRIGGER podokit_fill_legacy_account_issuer"))).toBe(
112
- true,
113
- );
114
- expect(statements.some((sql) => sql.includes('ALTER COLUMN "issuer" SET NOT NULL'))).toBe(true);
115
- expect(statements.some((sql) => sql.includes('CREATE UNIQUE INDEX IF NOT EXISTS'))).toBe(true);
116
- expect(statements.at(-1)).toBe("COMMIT");
42
+ it("relaxes legacy PostgreSQL constraints without rewriting account data", async (): Promise<void> => {
43
+ const client = new AccountClient();
44
+ expect(await migrate(client)).toBe("migrated");
45
+ expect(client.statements.some((sql) => sql.includes('ALTER COLUMN "issuer" DROP NOT NULL'))).toBe(true);
46
+ expect(client.statements.some((sql) => sql.startsWith("UPDATE") || sql.startsWith("DELETE"))).toBe(false);
47
+ expect(client.statements.at(-1)).toBe("COMMIT");
117
48
  expect(client.released).toBe(true);
118
49
  });
119
-
120
- it("rolls back instead of guessing a Microsoft or unknown issuer", async () => {
121
- const client = new LegacyAccountClient(["microsoft"]);
122
-
123
- await expect(migrateLegacyAccountIssuers(database(client))).rejects.toThrow(
124
- "Cannot infer a trusted Better Auth issuer for legacy provider microsoft",
125
- );
126
-
127
- expect(client.statements.map(({ sql }) => sql.trim())).toContain("ROLLBACK");
128
- expect(client.statements.some(({ sql }) => sql.includes('SET NOT NULL'))).toBe(false);
50
+ it("does not add issuer to a current PostgreSQL schema", async (): Promise<void> => {
51
+ const client = new AccountClient(true, false);
52
+ expect(await migrate(client)).toBe("current");
53
+ expect(client.statements.some((sql) => sql.includes("ADD COLUMN") || sql.includes("ALTER COLUMN"))).toBe(false);
54
+ });
55
+ it("rolls back PostgreSQL identity collisions before changing constraints", async (): Promise<void> => {
56
+ const client = new AccountClient(true, true, 1);
57
+ await expect(migrate(client)).rejects.toThrow("duplicate providerId and accountId");
58
+ expect(client.statements.at(-1)).toBe("ROLLBACK");
59
+ expect(client.statements.some((sql) => sql.startsWith("ALTER") || sql.startsWith("DROP"))).toBe(false);
129
60
  expect(client.released).toBe(true);
130
61
  });
131
-
132
- it("leaves a fresh database to the normal migrator and protects a current schema", async () => {
133
- const absent = new LegacyAccountClient([], { tableExists: false });
134
- const current = new LegacyAccountClient([], {
135
- column: { dataType: "text", isNullable: "NO" },
136
- });
137
-
138
- await expect(migrateLegacyAccountIssuers(database(absent))).resolves.toBe("absent");
139
- await expect(migrateLegacyAccountIssuers(database(current))).resolves.toBe("current");
140
-
141
- expect(absent.statements.some(({ sql }) => sql.includes("CREATE TRIGGER"))).toBe(false);
142
- expect(current.statements.some(({ sql }) => sql.includes("CREATE TRIGGER"))).toBe(true);
143
- for (const client of [absent, current]) {
144
- expect(client.statements.at(-1)?.sql.trim()).toBe("COMMIT");
145
- expect(client.released).toBe(true);
146
- }
62
+ it("leaves an absent SQLite table to the auth migrator", (): void => {
63
+ const database = new Database(":memory:");
64
+ try { expect(migrateSqliteLegacyAccountIssuers(database)).toBe("absent"); }
65
+ finally { database.close(); }
147
66
  });
148
-
149
- it("rolls back before constraints when credentials mismatch or identity keys collide", async () => {
150
- const mismatch = new LegacyAccountClient(["credential"], {
151
- counts: { credentialMismatch: 1 },
152
- });
153
- const collision = new LegacyAccountClient(["google"], {
154
- counts: { collision: 1 },
155
- });
156
-
157
- await expect(migrateLegacyAccountIssuers(database(mismatch))).rejects.toThrow(
158
- "accountId differs from the linked userId",
159
- );
160
- await expect(migrateLegacyAccountIssuers(database(collision))).rejects.toThrow(
161
- "collide on the new issuer and accountId identity key",
162
- );
163
-
164
- for (const client of [mismatch, collision]) {
165
- expect(client.statements.map(({ sql }) => sql.trim())).toContain("ROLLBACK");
166
- expect(client.statements.some(({ sql }) => sql.includes('SET NOT NULL'))).toBe(false);
167
- expect(client.released).toBe(true);
168
- }
67
+ it("preserves SQLite accounts, accepts new rows without issuer, and can rerun", (): void => {
68
+ const database = legacyDatabase();
69
+ try {
70
+ expect(migrateSqliteLegacyAccountIssuers(database)).toBe("migrated");
71
+ expect(database.query('SELECT "userId", password FROM account').get()).toEqual({ userId: "user", password: "password-hash" });
72
+ database.run('INSERT INTO account (id, "providerId", "accountId", "userId") VALUES (\'new\', \'github\', \'user\', \'user\')');
73
+ expect(migrateSqliteLegacyAccountIssuers(database)).toBe("current");
74
+ expect(database.query('SELECT count(*) AS count FROM account').get()).toEqual({ count: 2 });
75
+ expect(() => database.run('INSERT INTO account (id, "providerId", "accountId", "userId") VALUES (\'duplicate\', \'github\', \'user\', \'other\')')).toThrow();
76
+ } finally { database.close(); }
77
+ });
78
+ it("rolls back SQLite collisions without losing rows, issuer, or its index", (): void => {
79
+ const database = legacyDatabase();
80
+ try {
81
+ database.run('INSERT INTO account VALUES (\'collision\', \'credential\', \'user\', \'other\', \'other-hash\', \'other-issuer\')');
82
+ expect(() => migrateSqliteLegacyAccountIssuers(database)).toThrow("duplicate providerId and accountId");
83
+ expect(database.query('SELECT count(*) AS count FROM account').get()).toEqual({ count: 2 });
84
+ expect(database.query('SELECT issuer FROM account WHERE id = \'account\'').get()).toEqual({ issuer: "local:credential" });
85
+ expect(database.query('SELECT name FROM sqlite_master WHERE name = \'account_issuer_accountId_uidx\'').get()).toBeTruthy();
86
+ } finally { database.close(); }
169
87
  });
170
88
  });