@shaferllc/keel 0.82.0 → 0.83.1

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.
Files changed (150) hide show
  1. package/README.md +2 -2
  2. package/dist/billing/billable.d.ts +2 -3
  3. package/dist/billing/billable.js +2 -3
  4. package/dist/billing/billing.config.stub +1 -1
  5. package/dist/billing/builder.d.ts +2 -2
  6. package/dist/billing/builder.js +2 -2
  7. package/dist/billing/provider.d.ts +1 -1
  8. package/dist/billing/provider.js +2 -2
  9. package/dist/billing/subscription-item.d.ts +0 -1
  10. package/dist/billing/subscription-item.js +0 -1
  11. package/dist/core/decorators.d.ts +5 -7
  12. package/dist/core/decorators.js +5 -7
  13. package/dist/core/exceptions.d.ts +2 -3
  14. package/dist/core/exceptions.js +2 -3
  15. package/dist/core/model-query.d.ts +1 -1
  16. package/dist/core/model-query.js +1 -1
  17. package/dist/core/request-logger.d.ts +2 -2
  18. package/dist/core/request-logger.js +2 -2
  19. package/dist/core/request.js +1 -1
  20. package/dist/core/testing.d.ts +3 -3
  21. package/dist/core/testing.js +3 -3
  22. package/dist/db/d1-http.d.ts +34 -0
  23. package/dist/db/d1-http.js +61 -0
  24. package/dist/db/libsql.d.ts +13 -3
  25. package/dist/teams/models.js +51 -5
  26. package/docs/ai-manifest.json +23 -2
  27. package/docs/billing.md +5 -6
  28. package/docs/changelog.md +106 -21
  29. package/docs/database.md +5 -522
  30. package/docs/decorators.md +2 -6
  31. package/docs/errors.md +1 -1
  32. package/docs/hooks.md +1 -3
  33. package/docs/logger.md +2 -2
  34. package/docs/models.md +5 -2
  35. package/docs/orm.md +57 -0
  36. package/docs/packages.md +3 -3
  37. package/docs/providers.md +6 -6
  38. package/docs/query-builder.md +533 -0
  39. package/docs/request-response.md +2 -2
  40. package/docs/starter-kits.md +88 -0
  41. package/docs/testing.md +1 -2
  42. package/docs/validation.md +1 -2
  43. package/llms-full.txt +5301 -5126
  44. package/llms.txt +4 -1
  45. package/package.json +7 -2
  46. package/templates/api/.env.example +12 -0
  47. package/templates/api/app/Controllers/PostController.ts +37 -0
  48. package/templates/api/app/Http/Kernel.ts +13 -0
  49. package/templates/api/app/Http/Middleware/requestLogger.ts +8 -0
  50. package/templates/api/app/Models/Post.ts +12 -0
  51. package/templates/api/app/Providers/AppServiceProvider.ts +8 -0
  52. package/templates/api/app/Providers/DatabaseServiceProvider.ts +52 -0
  53. package/templates/api/bin/keel.ts +15 -0
  54. package/templates/api/bootstrap/app.ts +24 -0
  55. package/templates/api/bootstrap/providers.edge.ts +14 -0
  56. package/templates/api/bootstrap/providers.ts +7 -0
  57. package/templates/api/config/app.ts +10 -0
  58. package/templates/api/config/database.ts +42 -0
  59. package/templates/api/database/migrations/0001_create_posts.ts +20 -0
  60. package/templates/api/package.json +30 -0
  61. package/templates/api/routes/web.ts +12 -0
  62. package/templates/api/tests/posts.test.ts +30 -0
  63. package/templates/api/tsconfig.json +16 -0
  64. package/templates/api/worker.ts +33 -0
  65. package/templates/api/wrangler.jsonc +22 -0
  66. package/templates/app/.env.example +12 -0
  67. package/templates/app/app/Controllers/AuthController.ts +117 -0
  68. package/templates/app/app/Controllers/DashboardController.ts +37 -0
  69. package/templates/app/app/Controllers/HomeController.ts +10 -0
  70. package/templates/app/app/Http/Kernel.ts +21 -0
  71. package/templates/app/app/Http/Middleware/requestLogger.ts +8 -0
  72. package/templates/app/app/Models/User.ts +15 -0
  73. package/templates/app/app/Providers/AppServiceProvider.ts +12 -0
  74. package/templates/app/app/Providers/DatabaseServiceProvider.ts +52 -0
  75. package/templates/app/bin/keel.ts +15 -0
  76. package/templates/app/bootstrap/app.ts +24 -0
  77. package/templates/app/bootstrap/providers.edge.ts +15 -0
  78. package/templates/app/bootstrap/providers.ts +18 -0
  79. package/templates/app/config/app.ts +10 -0
  80. package/templates/app/config/database.ts +42 -0
  81. package/templates/app/database/migrations/0001_create_users.ts +21 -0
  82. package/templates/app/package.json +35 -0
  83. package/templates/app/public/.gitkeep +2 -0
  84. package/templates/app/resources/css/app.css +1 -0
  85. package/templates/app/resources/views/auth/forgot.tsx +30 -0
  86. package/templates/app/resources/views/auth/login.tsx +24 -0
  87. package/templates/app/resources/views/auth/register.tsx +24 -0
  88. package/templates/app/resources/views/auth/two-factor.tsx +22 -0
  89. package/templates/app/resources/views/dashboard.tsx +20 -0
  90. package/templates/app/resources/views/layout.tsx +15 -0
  91. package/templates/app/resources/views/welcome.tsx +29 -0
  92. package/templates/app/routes/web.ts +35 -0
  93. package/templates/app/tests/auth.test.ts +47 -0
  94. package/templates/app/tsconfig.json +31 -0
  95. package/templates/app/worker.ts +33 -0
  96. package/templates/app/wrangler.jsonc +25 -0
  97. package/templates/minimal/.env.example +8 -0
  98. package/templates/minimal/app/Controllers/HomeController.ts +14 -0
  99. package/templates/minimal/app/Http/Kernel.ts +22 -0
  100. package/templates/minimal/app/Http/Middleware/requestLogger.ts +8 -0
  101. package/templates/minimal/app/Providers/AppServiceProvider.ts +8 -0
  102. package/templates/minimal/bin/keel.ts +15 -0
  103. package/templates/minimal/bootstrap/app.ts +24 -0
  104. package/templates/minimal/bootstrap/providers.ts +6 -0
  105. package/templates/minimal/config/app.ts +10 -0
  106. package/templates/minimal/package.json +29 -0
  107. package/templates/minimal/public/.gitkeep +2 -0
  108. package/templates/minimal/resources/css/app.css +1 -0
  109. package/templates/minimal/resources/views/layout.tsx +15 -0
  110. package/templates/minimal/resources/views/welcome.tsx +15 -0
  111. package/templates/minimal/routes/web.ts +13 -0
  112. package/templates/minimal/tsconfig.json +18 -0
  113. package/templates/minimal/worker.ts +23 -0
  114. package/templates/minimal/wrangler.jsonc +10 -0
  115. package/templates/saas/.env.example +12 -0
  116. package/templates/saas/app/Controllers/AuthController.ts +125 -0
  117. package/templates/saas/app/Controllers/DashboardController.ts +37 -0
  118. package/templates/saas/app/Controllers/HomeController.ts +10 -0
  119. package/templates/saas/app/Controllers/TeamController.ts +88 -0
  120. package/templates/saas/app/Http/Kernel.ts +27 -0
  121. package/templates/saas/app/Http/Middleware/requestLogger.ts +8 -0
  122. package/templates/saas/app/Models/Project.ts +20 -0
  123. package/templates/saas/app/Models/User.ts +15 -0
  124. package/templates/saas/app/Providers/AppServiceProvider.ts +12 -0
  125. package/templates/saas/app/Providers/DatabaseServiceProvider.ts +52 -0
  126. package/templates/saas/bin/keel.ts +15 -0
  127. package/templates/saas/bootstrap/app.ts +24 -0
  128. package/templates/saas/bootstrap/providers.edge.ts +18 -0
  129. package/templates/saas/bootstrap/providers.ts +22 -0
  130. package/templates/saas/config/app.ts +10 -0
  131. package/templates/saas/config/database.ts +42 -0
  132. package/templates/saas/database/migrations/0001_create_users.ts +21 -0
  133. package/templates/saas/database/migrations/0002_create_projects.ts +20 -0
  134. package/templates/saas/package.json +35 -0
  135. package/templates/saas/public/.gitkeep +2 -0
  136. package/templates/saas/resources/css/app.css +1 -0
  137. package/templates/saas/resources/views/auth/forgot.tsx +30 -0
  138. package/templates/saas/resources/views/auth/login.tsx +24 -0
  139. package/templates/saas/resources/views/auth/register.tsx +24 -0
  140. package/templates/saas/resources/views/auth/two-factor.tsx +22 -0
  141. package/templates/saas/resources/views/dashboard.tsx +20 -0
  142. package/templates/saas/resources/views/layout.tsx +15 -0
  143. package/templates/saas/resources/views/teams/index.tsx +85 -0
  144. package/templates/saas/resources/views/welcome.tsx +29 -0
  145. package/templates/saas/routes/web.ts +44 -0
  146. package/templates/saas/tests/auth.test.ts +47 -0
  147. package/templates/saas/tests/teams.test.ts +27 -0
  148. package/templates/saas/tsconfig.json +31 -0
  149. package/templates/saas/worker.ts +33 -0
  150. package/templates/saas/wrangler.jsonc +25 -0
package/README.md CHANGED
@@ -160,7 +160,7 @@ src/core/ The framework
160
160
  src/db/ Database adapters (D1, Postgres, libSQL)
161
161
  src/api/ CRUD REST resources from a model
162
162
  src/openapi/ Generates an OpenAPI spec from the routes
163
- src/billing/ Subscription billing — Stripe + Paddle (Cashier port)
163
+ src/billing/ Subscription billing — Stripe + Paddle
164
164
  src/watch/ The debug dashboard
165
165
  src/mcp/ The MCP server (docs + API for AI agents)
166
166
  src/vite/ The Vite plugin
@@ -228,7 +228,7 @@ See [docs/architecture.md](./docs/architecture.md) for the full picture.
228
228
  | [Internationalization](./docs/i18n.md) | ICU messages, `Intl` formatters, locale detection |
229
229
  | [Pages](./docs/pages.md) | Page-based routing — a file is a route |
230
230
  | [Packages](./docs/packages.md) | Redistributable slices of an app: routes, migrations, commands |
231
- | [Billing](./docs/billing.md) | Subscriptions, charges & webhooks — Stripe + Paddle (Cashier port) |
231
+ | [Billing](./docs/billing.md) | Subscriptions, charges & webhooks — Stripe + Paddle |
232
232
  | [Watch](./docs/watch.md) | Debug dashboard — requests, queries, jobs, logs at `/watch` |
233
233
  | [Views](./docs/views.md) | Hono JSX components, layouts, the View service |
234
234
  | [Templates](./docs/templates.md) | `{{ }}` + `@`-tag templating engine, edge-safe |
@@ -1,7 +1,6 @@
1
1
  /**
2
- * The `Billable` mixin — Cashier's Billable trait, as a TypeScript mixin. Apply
3
- * it to a model to give that model a gateway customer, subscriptions, charges,
4
- * and checkout:
2
+ * The `Billable` mixin — apply it to a model to give that model a gateway
3
+ * customer, subscriptions, charges, and checkout:
5
4
  *
6
5
  * class User extends Billable(Model) {
7
6
  * static table = "users";
@@ -1,7 +1,6 @@
1
1
  /**
2
- * The `Billable` mixin — Cashier's Billable trait, as a TypeScript mixin. Apply
3
- * it to a model to give that model a gateway customer, subscriptions, charges,
4
- * and checkout:
2
+ * The `Billable` mixin — apply it to a model to give that model a gateway
3
+ * customer, subscriptions, charges, and checkout:
5
4
  *
6
5
  * class User extends Billable(Model) {
7
6
  * static table = "users";
@@ -1,7 +1,7 @@
1
1
  import { env } from "@shaferllc/keel/core";
2
2
 
3
3
  /**
4
- * Billing configuration — a Cashier-style port covering Stripe and Paddle.
4
+ * Billing configuration — subscriptions and charges, covering Stripe and Paddle.
5
5
  * Published with `keel vendor:publish --tag billing-config`.
6
6
  */
7
7
  export default {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The fluent subscription builder — Cashier's `newSubscription(...)->create()`.
3
- * The `Billable` mixin hands it a small `BillableTarget` (so this file needn't
2
+ * The fluent subscription builder — `newSubscription(...).create()`. The
3
+ * `Billable` mixin hands it a small `BillableTarget` (so this file needn't
4
4
  * import the mixin, avoiding a cycle); the builder shapes the request, calls the
5
5
  * gateway, and persists a local `Subscription` synced from the result.
6
6
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The fluent subscription builder — Cashier's `newSubscription(...)->create()`.
3
- * The `Billable` mixin hands it a small `BillableTarget` (so this file needn't
2
+ * The fluent subscription builder — `newSubscription(...).create()`. The
3
+ * `Billable` mixin hands it a small `BillableTarget` (so this file needn't
4
4
  * import the mixin, avoiding a cycle); the builder shapes the request, calls the
5
5
  * gateway, and persists a local `Subscription` synced from the result.
6
6
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Keel Billing — a Cashier-style port covering Stripe and Paddle, shipped as a
2
+ * Keel Billing — subscription billing covering Stripe and Paddle, shipped as a
3
3
  * Keel package. One line in `bootstrap/providers.ts` turns it on:
4
4
  *
5
5
  * app.register(BillingServiceProvider)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Keel Billing — a Cashier-style port covering Stripe and Paddle, shipped as a
2
+ * Keel Billing — subscription billing covering Stripe and Paddle, shipped as a
3
3
  * Keel package. One line in `bootstrap/providers.ts` turns it on:
4
4
  *
5
5
  * app.register(BillingServiceProvider)
@@ -29,7 +29,7 @@ export class BillingServiceProvider extends PackageProvider {
29
29
  registerDefaultGateways(this.manager);
30
30
  setBilling(this.manager);
31
31
  this.app.instance(BillingManager, this.manager);
32
- // Cashier targets the standard `users` billable table.
32
+ // Default billable table is `users`.
33
33
  this.migrations([billingMigration("users")]);
34
34
  this.publishes({ [join(here, "billing.config.stub")]: "config/billing.ts" }, "billing-config");
35
35
  }
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * One line of a subscription — a single price and its quantity. A single-price
3
3
  * subscription has exactly one item; a multi-product subscription has several.
4
- * Mirrors Cashier's `subscription_items` table.
5
4
  */
6
5
  import { Model } from "../core/model.js";
7
6
  import type { Casts } from "../core/casts.js";
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * One line of a subscription — a single price and its quantity. A single-price
3
3
  * subscription has exactly one item; a multi-product subscription has several.
4
- * Mirrors Cashier's `subscription_items` table.
5
4
  */
6
5
  import { Model } from "../core/model.js";
7
6
  export class SubscriptionItem extends Model {
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Request decorators — attach named, computed values to the current request,
3
- * resolved lazily and memoized for the life of that request. Inspired by
4
- * Fastify's `decorateRequest`, but without its footguns: you register a resolver
5
- * once, and Keel computes it on first access and caches it per request (keyed off
6
- * the Hono context via a WeakMap, so there's no shared-state leak between
7
- * requests and no V8 shape deopt to design around).
3
+ * resolved lazily and memoized for the life of that request. You register a
4
+ * resolver once, and Keel computes it on first access and caches it per request
5
+ * (keyed off the Hono context via a WeakMap, so there's no shared-state leak
6
+ * between requests and no V8 shape deopt to design around).
8
7
  *
9
8
  * decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
10
9
  *
@@ -18,8 +17,7 @@ import type { Context } from "hono";
18
17
  /** Resolves a decorator's value from the current request context. */
19
18
  export type RequestResolver<T = unknown> = (c: Context) => T | Promise<T>;
20
19
  /**
21
- * Register a request decorator. Throws if `name` is already registered — a
22
- * collision guard, like Fastify's.
20
+ * Register a request decorator. Throws if `name` is already registered.
23
21
  */
24
22
  export declare function decorateRequest<T>(name: string, resolver: RequestResolver<T>): void;
25
23
  /** Whether a request decorator has been registered. */
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * Request decorators — attach named, computed values to the current request,
3
- * resolved lazily and memoized for the life of that request. Inspired by
4
- * Fastify's `decorateRequest`, but without its footguns: you register a resolver
5
- * once, and Keel computes it on first access and caches it per request (keyed off
6
- * the Hono context via a WeakMap, so there's no shared-state leak between
7
- * requests and no V8 shape deopt to design around).
3
+ * resolved lazily and memoized for the life of that request. You register a
4
+ * resolver once, and Keel computes it on first access and caches it per request
5
+ * (keyed off the Hono context via a WeakMap, so there's no shared-state leak
6
+ * between requests and no V8 shape deopt to design around).
8
7
  *
9
8
  * decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
10
9
  *
@@ -27,8 +26,7 @@ function bag(c) {
27
26
  return b;
28
27
  }
29
28
  /**
30
- * Register a request decorator. Throws if `name` is already registered — a
31
- * collision guard, like Fastify's.
29
+ * Register a request decorator. Throws if `name` is already registered.
32
30
  */
33
31
  export function decorateRequest(name, resolver) {
34
32
  if (resolvers.has(name)) {
@@ -92,9 +92,8 @@ export declare class ServiceUnavailableException extends HttpException {
92
92
  }
93
93
  /**
94
94
  * Mint a reusable, coded `HttpException` subclass — the ergonomic way to define
95
- * app-specific errors with a stable, machine-readable `code`. Inspired by
96
- * `@fastify/error`. The `message` may carry `%s` placeholders, filled in order
97
- * from the constructor arguments.
95
+ * app-specific errors with a stable, machine-readable `code`. The `message` may
96
+ * carry `%s` placeholders, filled in order from the constructor arguments.
98
97
  *
99
98
  * const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
100
99
  * throw new InsufficientFunds("$40");
@@ -196,9 +196,8 @@ export class ServiceUnavailableException extends HttpException {
196
196
  }
197
197
  /**
198
198
  * Mint a reusable, coded `HttpException` subclass — the ergonomic way to define
199
- * app-specific errors with a stable, machine-readable `code`. Inspired by
200
- * `@fastify/error`. The `message` may carry `%s` placeholders, filled in order
201
- * from the constructor arguments.
199
+ * app-specific errors with a stable, machine-readable `code`. The `message` may
200
+ * carry `%s` placeholders, filled in order from the constructor arguments.
202
201
  *
203
202
  * const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
204
203
  * throw new InsufficientFunds("$40");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A model-aware query builder — what `Model.query()` sugar and eager loading are
3
3
  * built on. It wraps the plain `QueryBuilder`, hydrates rows into models (firing
4
- * `retrieved`), and adds the relationship-aware operations Eloquent has and a raw
4
+ * `retrieved`), and adds the relationship-aware operations a query builder lacks and a raw
5
5
  * builder can't: `with()` (nested eager loading), `withCount()`, and existence
6
6
  * filters `has()` / `whereHas()` / `doesntHave()`.
7
7
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A model-aware query builder — what `Model.query()` sugar and eager loading are
3
3
  * built on. It wraps the plain `QueryBuilder`, hydrates rows into models (firing
4
- * `retrieved`), and adds the relationship-aware operations Eloquent has and a raw
4
+ * `retrieved`), and adds the relationship-aware operations a query builder lacks and a raw
5
5
  * builder can't: `with()` (nested eager loading), `withCount()`, and existence
6
6
  * filters `has()` / `whereHas()` / `doesntHave()`.
7
7
  *
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Per-request logging. `requestLogger()` middleware binds a child logger to each
3
3
  * request with a generated `reqId`, so every log line within that request
4
- * correlates — Fastify's `request.log`, built on Keel's `Logger.child()`. It can
5
- * also log the request start and completion (method, path, status, duration).
4
+ * correlates — built on Keel's `Logger.child()`. It can also log the request
5
+ * start and completion (method, path, status, duration).
6
6
  *
7
7
  * kernel.use(requestLogger()); // in app/Http/Kernel.ts
8
8
  * requestLog().info("charging card"); // anywhere in the request → carries reqId
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Per-request logging. `requestLogger()` middleware binds a child logger to each
3
3
  * request with a generated `reqId`, so every log line within that request
4
- * correlates — Fastify's `request.log`, built on Keel's `Logger.child()`. It can
5
- * also log the request start and completion (method, path, status, duration).
4
+ * correlates — built on Keel's `Logger.child()`. It can also log the request
5
+ * start and completion (method, path, status, duration).
6
6
  *
7
7
  * kernel.use(requestLogger()); // in app/Http/Kernel.ts
8
8
  * requestLog().info("charging card"); // anywhere in the request → carries reqId
@@ -112,7 +112,7 @@ export function html(body, status) {
112
112
  }
113
113
  export function redirect(location, status) {
114
114
  const c = maybeCtx();
115
- // Koa-style `redirect("back")`: bounce to the Referer, or "/" if there isn't one.
115
+ // `redirect("back")`: bounce to the Referer, or "/" if there isn't one.
116
116
  if (location === "back")
117
117
  location = c?.req.header("referer") ?? "/";
118
118
  return c
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * A test client for Keel apps — inject requests without a live server and assert
3
- * on the response, the way Fastify's `inject()` does. Wraps the app's Hono
4
- * instance (which already does fetch-style request injection), adding verb
5
- * helpers with JSON bodies and fluent response assertions.
3
+ * on the response. Wraps the app's Hono instance (which already does fetch-style
4
+ * request injection), adding verb helpers with JSON bodies and fluent response
5
+ * assertions.
6
6
  *
7
7
  * const client = await testClient(app);
8
8
  * const res = await client.post("/users", { email: "a@b.com" });
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * A test client for Keel apps — inject requests without a live server and assert
3
- * on the response, the way Fastify's `inject()` does. Wraps the app's Hono
4
- * instance (which already does fetch-style request injection), adding verb
5
- * helpers with JSON bodies and fluent response assertions.
3
+ * on the response. Wraps the app's Hono instance (which already does fetch-style
4
+ * request injection), adding verb helpers with JSON bodies and fluent response
5
+ * assertions.
6
6
  *
7
7
  * const client = await testClient(app);
8
8
  * const res = await client.post("/users", { email: "a@b.com" });
@@ -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
+ }
@@ -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, Row } from "../core/database.js";
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: unknown[];
31
+ args: any[];
22
32
  }): Promise<{
23
- rows: Row[];
33
+ rows: any[];
24
34
  rowsAffected: number;
25
35
  lastInsertRowid?: bigint | number;
26
36
  }>;
@@ -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
- const team = await Team.create({
60
- name,
61
- slug: slug ?? slugify(name),
62
- owner_id: ownerId,
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()
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaferllc/keel",
3
- "version": "0.82.0",
3
+ "version": "0.83.1",
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",
@@ -50,7 +50,7 @@
50
50
  {
51
51
  "slug": "billing",
52
52
  "title": "Billing",
53
- "summary": "Keel Billing is a subscription-billing layer — a port of Laravel Cashier — for charging customers, managing subscriptions, and reconciling gateway state through webhooks.",
53
+ "summary": "Keel Billing is a subscription-billing layer for charging customers, managing subscriptions, and reconciling gateway state through webhooks.",
54
54
  "path": "docs/billing.md",
55
55
  "example": null
56
56
  },
@@ -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",
package/docs/billing.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # Billing
2
2
 
3
- Keel Billing is a subscription-billing layer a port of [Laravel
4
- Cashier](https://laravel.com/docs/13.x/billing) for charging customers,
5
- managing subscriptions, and reconciling gateway state through webhooks. It ships
6
- as a Keel [package](./packages.md) and supports two gateways behind one API:
3
+ Keel Billing is a subscription-billing layer for charging customers, managing
4
+ subscriptions, and reconciling gateway state through webhooks. It ships as a
5
+ Keel [package](./packages.md) and supports two gateways behind one API:
7
6
  **Stripe** and **Paddle**.
8
7
 
9
8
  It attaches to a model with a mixin. Your `User` becomes billable, gains a
@@ -221,8 +220,8 @@ resolveBillableUsing(async (customerId) => {
221
220
  The migration is gateway-neutral: `subscriptions` (with `gateway`,
222
221
  `provider_id`, `provider_status`, `provider_price`, trial/grace timestamps),
223
222
  `subscription_items`, and columns on `users` (`billing_gateway`,
224
- `billing_customer_id`, `pm_type`, `pm_last_four`, `trial_ends_at`). Cashier
225
- targets the standard `users` billable table.
223
+ `billing_customer_id`, `pm_type`, `pm_last_four`, `trial_ends_at`). The default
224
+ migration targets the standard `users` billable table.
226
225
 
227
226
  ## Testing
228
227