@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.
- package/README.md +2 -2
- package/dist/billing/billable.d.ts +2 -3
- package/dist/billing/billable.js +2 -3
- package/dist/billing/billing.config.stub +1 -1
- package/dist/billing/builder.d.ts +2 -2
- package/dist/billing/builder.js +2 -2
- package/dist/billing/provider.d.ts +1 -1
- package/dist/billing/provider.js +2 -2
- package/dist/billing/subscription-item.d.ts +0 -1
- package/dist/billing/subscription-item.js +0 -1
- package/dist/core/decorators.d.ts +5 -7
- package/dist/core/decorators.js +5 -7
- package/dist/core/exceptions.d.ts +2 -3
- package/dist/core/exceptions.js +2 -3
- package/dist/core/model-query.d.ts +1 -1
- package/dist/core/model-query.js +1 -1
- package/dist/core/request-logger.d.ts +2 -2
- package/dist/core/request-logger.js +2 -2
- package/dist/core/request.js +1 -1
- package/dist/core/testing.d.ts +3 -3
- package/dist/core/testing.js +3 -3
- 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 +23 -2
- package/docs/billing.md +5 -6
- package/docs/changelog.md +106 -21
- package/docs/database.md +5 -522
- package/docs/decorators.md +2 -6
- package/docs/errors.md +1 -1
- package/docs/hooks.md +1 -3
- package/docs/logger.md +2 -2
- package/docs/models.md +5 -2
- package/docs/orm.md +57 -0
- package/docs/packages.md +3 -3
- package/docs/providers.md +6 -6
- package/docs/query-builder.md +533 -0
- package/docs/request-response.md +2 -2
- package/docs/starter-kits.md +88 -0
- package/docs/testing.md +1 -2
- package/docs/validation.md +1 -2
- package/llms-full.txt +5301 -5126
- package/llms.txt +4 -1
- 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
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
|
|
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
|
|
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 —
|
|
3
|
-
*
|
|
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";
|
package/dist/billing/billable.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `Billable` mixin —
|
|
3
|
-
*
|
|
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 —
|
|
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 —
|
|
3
|
-
*
|
|
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
|
*
|
package/dist/billing/builder.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The fluent subscription builder —
|
|
3
|
-
*
|
|
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 —
|
|
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)
|
package/dist/billing/provider.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Keel Billing —
|
|
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
|
-
//
|
|
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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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. */
|
package/dist/core/decorators.js
CHANGED
|
@@ -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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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`.
|
|
96
|
-
*
|
|
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");
|
package/dist/core/exceptions.js
CHANGED
|
@@ -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`.
|
|
200
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/core/model-query.js
CHANGED
|
@@ -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
|
|
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 —
|
|
5
|
-
*
|
|
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 —
|
|
5
|
-
*
|
|
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
|
package/dist/core/request.js
CHANGED
|
@@ -112,7 +112,7 @@ export function html(body, status) {
|
|
|
112
112
|
}
|
|
113
113
|
export function redirect(location, status) {
|
|
114
114
|
const c = maybeCtx();
|
|
115
|
-
//
|
|
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
|
package/dist/core/testing.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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" });
|
package/dist/core/testing.js
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
+
}
|
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.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
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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`).
|
|
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
|
|