@shaferllc/keel 0.82.0 → 0.83.0
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/dist/db/d1-http.d.ts +34 -0
- package/dist/db/d1-http.js +61 -0
- package/dist/db/libsql.d.ts +13 -3
- package/dist/teams/models.js +51 -5
- package/docs/ai-manifest.json +22 -1
- package/docs/database.md +5 -522
- package/docs/models.md +5 -2
- package/docs/orm.md +57 -0
- package/docs/query-builder.md +533 -0
- package/docs/starter-kits.md +88 -0
- package/llms-full.txt +5299 -5115
- package/llms.txt +3 -0
- package/package.json +7 -2
- package/templates/api/.env.example +12 -0
- package/templates/api/app/Controllers/PostController.ts +37 -0
- package/templates/api/app/Http/Kernel.ts +13 -0
- package/templates/api/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/api/app/Models/Post.ts +12 -0
- package/templates/api/app/Providers/AppServiceProvider.ts +8 -0
- package/templates/api/app/Providers/DatabaseServiceProvider.ts +52 -0
- package/templates/api/bin/keel.ts +15 -0
- package/templates/api/bootstrap/app.ts +24 -0
- package/templates/api/bootstrap/providers.edge.ts +14 -0
- package/templates/api/bootstrap/providers.ts +7 -0
- package/templates/api/config/app.ts +10 -0
- package/templates/api/config/database.ts +42 -0
- package/templates/api/database/migrations/0001_create_posts.ts +20 -0
- package/templates/api/package.json +30 -0
- package/templates/api/routes/web.ts +12 -0
- package/templates/api/tests/posts.test.ts +30 -0
- package/templates/api/tsconfig.json +16 -0
- package/templates/api/worker.ts +33 -0
- package/templates/api/wrangler.jsonc +22 -0
- package/templates/app/.env.example +12 -0
- package/templates/app/app/Controllers/AuthController.ts +117 -0
- package/templates/app/app/Controllers/DashboardController.ts +37 -0
- package/templates/app/app/Controllers/HomeController.ts +10 -0
- package/templates/app/app/Http/Kernel.ts +21 -0
- package/templates/app/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/app/app/Models/User.ts +15 -0
- package/templates/app/app/Providers/AppServiceProvider.ts +12 -0
- package/templates/app/app/Providers/DatabaseServiceProvider.ts +52 -0
- package/templates/app/bin/keel.ts +15 -0
- package/templates/app/bootstrap/app.ts +24 -0
- package/templates/app/bootstrap/providers.edge.ts +15 -0
- package/templates/app/bootstrap/providers.ts +18 -0
- package/templates/app/config/app.ts +10 -0
- package/templates/app/config/database.ts +42 -0
- package/templates/app/database/migrations/0001_create_users.ts +21 -0
- package/templates/app/package.json +35 -0
- package/templates/app/public/.gitkeep +2 -0
- package/templates/app/resources/css/app.css +1 -0
- package/templates/app/resources/views/auth/forgot.tsx +30 -0
- package/templates/app/resources/views/auth/login.tsx +24 -0
- package/templates/app/resources/views/auth/register.tsx +24 -0
- package/templates/app/resources/views/auth/two-factor.tsx +22 -0
- package/templates/app/resources/views/dashboard.tsx +20 -0
- package/templates/app/resources/views/layout.tsx +15 -0
- package/templates/app/resources/views/welcome.tsx +29 -0
- package/templates/app/routes/web.ts +35 -0
- package/templates/app/tests/auth.test.ts +47 -0
- package/templates/app/tsconfig.json +31 -0
- package/templates/app/worker.ts +33 -0
- package/templates/app/wrangler.jsonc +25 -0
- package/templates/minimal/.env.example +8 -0
- package/templates/minimal/app/Controllers/HomeController.ts +14 -0
- package/templates/minimal/app/Http/Kernel.ts +22 -0
- package/templates/minimal/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/minimal/app/Providers/AppServiceProvider.ts +8 -0
- package/templates/minimal/bin/keel.ts +15 -0
- package/templates/minimal/bootstrap/app.ts +24 -0
- package/templates/minimal/bootstrap/providers.ts +6 -0
- package/templates/minimal/config/app.ts +10 -0
- package/templates/minimal/package.json +29 -0
- package/templates/minimal/public/.gitkeep +2 -0
- package/templates/minimal/resources/css/app.css +1 -0
- package/templates/minimal/resources/views/layout.tsx +15 -0
- package/templates/minimal/resources/views/welcome.tsx +15 -0
- package/templates/minimal/routes/web.ts +13 -0
- package/templates/minimal/tsconfig.json +18 -0
- package/templates/minimal/worker.ts +23 -0
- package/templates/minimal/wrangler.jsonc +10 -0
- package/templates/saas/.env.example +12 -0
- package/templates/saas/app/Controllers/AuthController.ts +125 -0
- package/templates/saas/app/Controllers/DashboardController.ts +37 -0
- package/templates/saas/app/Controllers/HomeController.ts +10 -0
- package/templates/saas/app/Controllers/TeamController.ts +88 -0
- package/templates/saas/app/Http/Kernel.ts +27 -0
- package/templates/saas/app/Http/Middleware/requestLogger.ts +8 -0
- package/templates/saas/app/Models/Project.ts +20 -0
- package/templates/saas/app/Models/User.ts +15 -0
- package/templates/saas/app/Providers/AppServiceProvider.ts +12 -0
- package/templates/saas/app/Providers/DatabaseServiceProvider.ts +52 -0
- package/templates/saas/bin/keel.ts +15 -0
- package/templates/saas/bootstrap/app.ts +24 -0
- package/templates/saas/bootstrap/providers.edge.ts +18 -0
- package/templates/saas/bootstrap/providers.ts +22 -0
- package/templates/saas/config/app.ts +10 -0
- package/templates/saas/config/database.ts +42 -0
- package/templates/saas/database/migrations/0001_create_users.ts +21 -0
- package/templates/saas/database/migrations/0002_create_projects.ts +20 -0
- package/templates/saas/package.json +35 -0
- package/templates/saas/public/.gitkeep +2 -0
- package/templates/saas/resources/css/app.css +1 -0
- package/templates/saas/resources/views/auth/forgot.tsx +30 -0
- package/templates/saas/resources/views/auth/login.tsx +24 -0
- package/templates/saas/resources/views/auth/register.tsx +24 -0
- package/templates/saas/resources/views/auth/two-factor.tsx +22 -0
- package/templates/saas/resources/views/dashboard.tsx +20 -0
- package/templates/saas/resources/views/layout.tsx +15 -0
- package/templates/saas/resources/views/teams/index.tsx +85 -0
- package/templates/saas/resources/views/welcome.tsx +29 -0
- package/templates/saas/routes/web.ts +44 -0
- package/templates/saas/tests/auth.test.ts +47 -0
- package/templates/saas/tests/teams.test.ts +27 -0
- package/templates/saas/tsconfig.json +31 -0
- package/templates/saas/worker.ts +33 -0
- package/templates/saas/wrangler.jsonc +25 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Keel `Connection` for Cloudflare D1 over its **HTTP API**.
|
|
3
|
+
*
|
|
4
|
+
* The D1 binding (`env.DB`) only exists inside a running Worker, which leaves an
|
|
5
|
+
* awkward hole: `keel migrate` runs on your laptop and in CI, where there is no
|
|
6
|
+
* binding — so there was no way to create your tables. This closes it. Same
|
|
7
|
+
* `Connection` interface, so migrations, models, and the query builder all work
|
|
8
|
+
* against a real D1 database from anywhere:
|
|
9
|
+
*
|
|
10
|
+
* import { d1HttpConnection } from "@shaferllc/keel/db/d1-http";
|
|
11
|
+
*
|
|
12
|
+
* setConnection(d1HttpConnection({
|
|
13
|
+
* accountId: env("CLOUDFLARE_ACCOUNT_ID"),
|
|
14
|
+
* databaseId: env("D1_DATABASE_ID"),
|
|
15
|
+
* apiToken: env("CLOUDFLARE_API_TOKEN"),
|
|
16
|
+
* }), "sqlite");
|
|
17
|
+
*
|
|
18
|
+
* Use the **binding** (`@shaferllc/keel/db/d1`) inside the Worker — it's a direct
|
|
19
|
+
* call with no network hop. Use this one for migrations and scripts. Both speak
|
|
20
|
+
* SQLite, so the same schema serves both.
|
|
21
|
+
*
|
|
22
|
+
* `fetch` only — no SDK, nothing Node-specific.
|
|
23
|
+
*/
|
|
24
|
+
import type { Connection } from "../core/database.js";
|
|
25
|
+
export interface D1HttpOptions {
|
|
26
|
+
accountId: string;
|
|
27
|
+
databaseId: string;
|
|
28
|
+
/** An API token with D1 edit permission. */
|
|
29
|
+
apiToken: string;
|
|
30
|
+
/** Override the API base (for tests, or a proxy). */
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Build a `Connection` that talks to D1 over HTTP. */
|
|
34
|
+
export declare function d1HttpConnection(options: D1HttpOptions): Connection;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Keel `Connection` for Cloudflare D1 over its **HTTP API**.
|
|
3
|
+
*
|
|
4
|
+
* The D1 binding (`env.DB`) only exists inside a running Worker, which leaves an
|
|
5
|
+
* awkward hole: `keel migrate` runs on your laptop and in CI, where there is no
|
|
6
|
+
* binding — so there was no way to create your tables. This closes it. Same
|
|
7
|
+
* `Connection` interface, so migrations, models, and the query builder all work
|
|
8
|
+
* against a real D1 database from anywhere:
|
|
9
|
+
*
|
|
10
|
+
* import { d1HttpConnection } from "@shaferllc/keel/db/d1-http";
|
|
11
|
+
*
|
|
12
|
+
* setConnection(d1HttpConnection({
|
|
13
|
+
* accountId: env("CLOUDFLARE_ACCOUNT_ID"),
|
|
14
|
+
* databaseId: env("D1_DATABASE_ID"),
|
|
15
|
+
* apiToken: env("CLOUDFLARE_API_TOKEN"),
|
|
16
|
+
* }), "sqlite");
|
|
17
|
+
*
|
|
18
|
+
* Use the **binding** (`@shaferllc/keel/db/d1`) inside the Worker — it's a direct
|
|
19
|
+
* call with no network hop. Use this one for migrations and scripts. Both speak
|
|
20
|
+
* SQLite, so the same schema serves both.
|
|
21
|
+
*
|
|
22
|
+
* `fetch` only — no SDK, nothing Node-specific.
|
|
23
|
+
*/
|
|
24
|
+
/** Build a `Connection` that talks to D1 over HTTP. */
|
|
25
|
+
export function d1HttpConnection(options) {
|
|
26
|
+
const base = options.baseUrl ?? "https://api.cloudflare.com/client/v4";
|
|
27
|
+
const url = `${base}/accounts/${options.accountId}/d1/database/${options.databaseId}/query`;
|
|
28
|
+
async function query(sql, params) {
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: `Bearer ${options.apiToken}`,
|
|
33
|
+
"content-type": "application/json",
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({ sql, params }),
|
|
36
|
+
});
|
|
37
|
+
const payload = (await response.json());
|
|
38
|
+
if (!response.ok || !payload.success) {
|
|
39
|
+
// Cloudflare returns its errors in the body with a 200 as often as not, so
|
|
40
|
+
// the status alone is not enough to tell whether the statement ran.
|
|
41
|
+
const message = payload.errors?.map((e) => `${e.code}: ${e.message}`).join("; ") ??
|
|
42
|
+
`D1 request failed (${response.status})`;
|
|
43
|
+
throw new Error(`D1: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
return payload.result;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
async select(sql, bindings) {
|
|
49
|
+
const result = await query(sql, bindings);
|
|
50
|
+
return result?.[0]?.results ?? [];
|
|
51
|
+
},
|
|
52
|
+
async write(sql, bindings) {
|
|
53
|
+
const result = await query(sql, bindings);
|
|
54
|
+
const meta = result?.[0]?.meta ?? {};
|
|
55
|
+
return {
|
|
56
|
+
rowsAffected: meta.changes ?? meta.rows_written ?? 0,
|
|
57
|
+
insertId: meta.last_row_id != null ? Number(meta.last_row_id) : undefined,
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/db/libsql.d.ts
CHANGED
|
@@ -13,14 +13,24 @@
|
|
|
13
13
|
* The client is duck-typed — this module imports no driver, so it never bundles
|
|
14
14
|
* `@libsql/client`.
|
|
15
15
|
*/
|
|
16
|
-
import type { Connection
|
|
16
|
+
import type { Connection } from "../core/database.js";
|
|
17
17
|
/** The slice of the `@libsql/client` API this adapter uses. */
|
|
18
18
|
export interface LibSqlLike {
|
|
19
|
+
/**
|
|
20
|
+
* `any` rather than `unknown[]` / `Row[]` on purpose.
|
|
21
|
+
*
|
|
22
|
+
* The official client types its arguments as `InArgs` and its rows as its own
|
|
23
|
+
* `Row`, and under `strictFunctionTypes` a narrower parameter type makes the real
|
|
24
|
+
* `Client` *unassignable* to this interface — so wiring libSQL the obvious way
|
|
25
|
+
* required `client as unknown as LibSqlLike`, which is a cast a user shouldn't
|
|
26
|
+
* have to discover. Widening here keeps `libsqlConnection(createClient(…))`
|
|
27
|
+
* working with no ceremony; the values are normalized below anyway.
|
|
28
|
+
*/
|
|
19
29
|
execute(stmt: {
|
|
20
30
|
sql: string;
|
|
21
|
-
args:
|
|
31
|
+
args: any[];
|
|
22
32
|
}): Promise<{
|
|
23
|
-
rows:
|
|
33
|
+
rows: any[];
|
|
24
34
|
rowsAffected: number;
|
|
25
35
|
lastInsertRowid?: bigint | number;
|
|
26
36
|
}>;
|
package/dist/teams/models.js
CHANGED
|
@@ -56,14 +56,38 @@ export async function memberOf(userId, teamId, atLeast = "member") {
|
|
|
56
56
|
* leave the owner out of every membership query.
|
|
57
57
|
*/
|
|
58
58
|
export async function createTeam(name, ownerId, slug) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
let team;
|
|
60
|
+
// Retry on the unique index, don't just look before leaping.
|
|
61
|
+
//
|
|
62
|
+
// Picking a free slug with a SELECT is a check-then-act race: two people called
|
|
63
|
+
// Ada signing up at the same moment both see "ada-s-team" is free, and one of them
|
|
64
|
+
// gets a constraint error instead of an account. The index is the only real
|
|
65
|
+
// arbiter, so the fix is to let it arbitrate and try again — not to look harder.
|
|
66
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
67
|
+
const candidate = slug ?? (await uniqueSlug(slugify(name)));
|
|
68
|
+
try {
|
|
69
|
+
team = (await Team.create({ name, slug: candidate, owner_id: ownerId }));
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// An explicit slug was asked for and taken: that's the caller's problem.
|
|
74
|
+
if (slug || !isUniqueViolation(error))
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!team)
|
|
79
|
+
throw new Error(`Could not find a free slug for "${name}".`);
|
|
64
80
|
await Membership.create({ team_id: team.id, user_id: ownerId, role: "owner" });
|
|
65
81
|
return team;
|
|
66
82
|
}
|
|
83
|
+
/** Every driver phrases it differently, and none of them agree on an error code. */
|
|
84
|
+
function isUniqueViolation(error) {
|
|
85
|
+
const message = String(error?.message ?? error);
|
|
86
|
+
return (/unique/i.test(message) || // sqlite / libsql / d1
|
|
87
|
+
/duplicate key/i.test(message) || // postgres
|
|
88
|
+
/duplicate entry/i.test(message) // mysql
|
|
89
|
+
);
|
|
90
|
+
}
|
|
67
91
|
/**
|
|
68
92
|
* Switch which team a user is acting as — **only** to a team they're actually in.
|
|
69
93
|
*
|
|
@@ -77,6 +101,28 @@ export async function switchTeam(userId, teamId, userTable = "users") {
|
|
|
77
101
|
await db(userTable).where("id", userId).update({ current_team_id: teamId });
|
|
78
102
|
return true;
|
|
79
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* A slug nobody else has taken.
|
|
106
|
+
*
|
|
107
|
+
* `teams.slug` is unique, and personal teams are named after their owner — so two
|
|
108
|
+
* people called Ada would collide and the second one's signup would 500. Names are
|
|
109
|
+
* not unique and were never going to be; the slug has to make itself so.
|
|
110
|
+
*/
|
|
111
|
+
async function uniqueSlug(base) {
|
|
112
|
+
const stem = base || "team";
|
|
113
|
+
// The unique index is still the real guarantee; this just avoids the collision in
|
|
114
|
+
// the common case rather than surfacing a constraint error to someone signing up.
|
|
115
|
+
const taken = new Set((await db(Team.table).where("slug", "like", `${stem}%`).get()).map((row) => String(row.slug)));
|
|
116
|
+
if (!taken.has(stem))
|
|
117
|
+
return stem;
|
|
118
|
+
for (let n = 2; n < 1000; n++) {
|
|
119
|
+
const candidate = `${stem}-${n}`;
|
|
120
|
+
if (!taken.has(candidate))
|
|
121
|
+
return candidate;
|
|
122
|
+
}
|
|
123
|
+
// A thousand teams called the same thing. Fine — stop counting.
|
|
124
|
+
return `${stem}-${crypto.randomUUID().slice(0, 8)}`;
|
|
125
|
+
}
|
|
80
126
|
function slugify(name) {
|
|
81
127
|
return name
|
|
82
128
|
.toLowerCase()
|
package/docs/ai-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shaferllc/keel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.83.0",
|
|
4
4
|
"description": "The house framework for Node.js — a service container, providers, routing, JSX views, and a code-generating console.",
|
|
5
5
|
"repo": "https://github.com/shaferllc/keel",
|
|
6
6
|
"generated": "run `npm run build:ai` to regenerate",
|
|
@@ -271,6 +271,13 @@
|
|
|
271
271
|
"path": "docs/openapi.md",
|
|
272
272
|
"example": null
|
|
273
273
|
},
|
|
274
|
+
{
|
|
275
|
+
"slug": "orm",
|
|
276
|
+
"title": "ORM",
|
|
277
|
+
"summary": "Keel's ORM is a compact active record over the query builder: a model is a class pointed at a table, and its rows come back as typed objects with methods.",
|
|
278
|
+
"path": "docs/orm.md",
|
|
279
|
+
"example": null
|
|
280
|
+
},
|
|
274
281
|
{
|
|
275
282
|
"slug": "packages",
|
|
276
283
|
"title": "Packages",
|
|
@@ -292,6 +299,13 @@
|
|
|
292
299
|
"path": "docs/providers.md",
|
|
293
300
|
"example": "docs/examples/providers.ts"
|
|
294
301
|
},
|
|
302
|
+
{
|
|
303
|
+
"slug": "query-builder",
|
|
304
|
+
"title": "Query Builder",
|
|
305
|
+
"summary": "Keel's driver-agnostic query builder — build and run SQL by chaining methods off db(table).",
|
|
306
|
+
"path": "docs/query-builder.md",
|
|
307
|
+
"example": null
|
|
308
|
+
},
|
|
295
309
|
{
|
|
296
310
|
"slug": "queues",
|
|
297
311
|
"title": "Queues & Jobs",
|
|
@@ -355,6 +369,13 @@
|
|
|
355
369
|
"path": "docs/social-auth.md",
|
|
356
370
|
"example": null
|
|
357
371
|
},
|
|
372
|
+
{
|
|
373
|
+
"slug": "starter-kits",
|
|
374
|
+
"title": "Starter kits",
|
|
375
|
+
"summary": "",
|
|
376
|
+
"path": "docs/starter-kits.md",
|
|
377
|
+
"example": null
|
|
378
|
+
},
|
|
358
379
|
{
|
|
359
380
|
"slug": "static-files",
|
|
360
381
|
"title": "Static Files",
|