@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 +60 -6
- package/dist/templates/base/AGENTS.md +9 -3
- package/dist/templates/base/README.md +12 -8
- package/dist/templates/fullstack/AGENTS.md +20 -2
- package/dist/templates/fullstack/README.md +65 -8
- package/dist/templates/fullstack/apps/api/package.json +1 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.spec.ts +66 -148
- package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.ts +60 -197
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +2 -0
- package/dist/templates/modules/auth/module.manifest.json +5 -5
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +7 -3
- package/dist/templates/todo/AGENTS.md +24 -5
- package/dist/templates/todo/README.md +64 -11
- package/dist/templates/todo/apps/api/src/migrations/1720100000000-InitTodos.ts +11 -0
- package/dist/templates/todo/apps/api/src/todos/todo.repository.spec.ts +76 -0
- package/dist/templates/todo/apps/api/src/todos/todo.repository.ts +9 -4
- package/dist/templates/todo/tests/api/todo.api.spec.ts +32 -0
- package/dist/templates/todo/tests/ui/todo.ui.spec.ts +31 -0
- package/package.json +1 -1
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
|

|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
9
|
-
|
|
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
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
114
|
+
{{apiRun}} dev # terminal 1
|
|
115
|
+
```
|
|
65
116
|
|
|
66
|
-
|
|
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
|
|
75
|
-
|
|
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",
|
package/dist/templates/modules/auth/files/apps/api/src/auth/account-issuer-migration.spec.ts
CHANGED
|
@@ -1,170 +1,88 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
expect(
|
|
88
|
-
expect(
|
|
89
|
-
expect(
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
expect(client.statements.
|
|
128
|
-
expect(client.statements.some((
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
expect(
|
|
166
|
-
expect(
|
|
167
|
-
|
|
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
|
});
|