@cfast/db 0.0.1 → 0.1.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/index.d.ts +24 -3
- package/dist/index.js +18 -1
- package/llms.txt +186 -0
- package/package.json +20 -11
- package/LICENSE +0 -21
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ type Operation<TResult> = {
|
|
|
31
31
|
*
|
|
32
32
|
* @param params - Placeholder values for `sql.placeholder()` calls. Pass `{}` when no placeholders are used.
|
|
33
33
|
*/
|
|
34
|
-
run: (params
|
|
34
|
+
run: (params?: Record<string, unknown>) => Promise<TResult>;
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
37
|
* Supported cache backend types for {@link CacheConfig}.
|
|
@@ -542,7 +542,7 @@ declare function createDb(config: DbConfig): Db;
|
|
|
542
542
|
* Each `RunFn` corresponds to one of the operations passed to `compose()`,
|
|
543
543
|
* preserving the same positional order.
|
|
544
544
|
*/
|
|
545
|
-
type RunFn = (params
|
|
545
|
+
type RunFn = (params?: Record<string, unknown>) => Promise<unknown>;
|
|
546
546
|
/**
|
|
547
547
|
* Merges multiple {@link Operation | Operations} into a single operation with combined,
|
|
548
548
|
* deduplicated permissions and an executor function for controlling data flow.
|
|
@@ -579,6 +579,27 @@ type RunFn = (params: Record<string, unknown>) => Promise<unknown>;
|
|
|
579
579
|
* ```
|
|
580
580
|
*/
|
|
581
581
|
declare function compose<TResult>(operations: Operation<unknown>[], executor: (...runs: RunFn[]) => TResult | Promise<TResult>): Operation<TResult>;
|
|
582
|
+
/**
|
|
583
|
+
* Runs multiple {@link Operation | Operations} sequentially and returns their results as an array.
|
|
584
|
+
*
|
|
585
|
+
* This is a shorthand for the common `compose` pattern where operations are
|
|
586
|
+
* simply awaited in order with no data dependencies between them:
|
|
587
|
+
*
|
|
588
|
+
* ```ts
|
|
589
|
+
* // Before: verbose
|
|
590
|
+
* compose([op1, op2], async (run1, run2) => {
|
|
591
|
+
* await run1({});
|
|
592
|
+
* await run2({});
|
|
593
|
+
* });
|
|
594
|
+
*
|
|
595
|
+
* // After: concise
|
|
596
|
+
* composeSequential([op1, op2]);
|
|
597
|
+
* ```
|
|
598
|
+
*
|
|
599
|
+
* @param operations - The operations to run in order.
|
|
600
|
+
* @returns A single {@link Operation} that runs all operations sequentially and returns their results.
|
|
601
|
+
*/
|
|
602
|
+
declare function composeSequential(operations: Operation<unknown>[]): Operation<unknown[]>;
|
|
582
603
|
|
|
583
604
|
/**
|
|
584
605
|
* Options for controlling default and maximum pagination limits.
|
|
@@ -633,4 +654,4 @@ declare function parseCursorParams(request: Request, options?: PaginationOptions
|
|
|
633
654
|
*/
|
|
634
655
|
declare function parseOffsetParams(request: Request, options?: PaginationOptions): OffsetParams;
|
|
635
656
|
|
|
636
|
-
export { type CacheBackend, type CacheConfig, type CursorPage, type CursorParams, type Db, type DbConfig, type DeleteBuilder, type DeleteReturningBuilder, type FindFirstOptions, type FindManyOptions, type InsertBuilder, type InsertReturningBuilder, type OffsetPage, type OffsetParams, type Operation, type PaginateOptions, type PaginateParams, type QueryBuilder, type QueryCacheOptions, type UpdateBuilder, type UpdateReturningBuilder, type UpdateWhereBuilder, compose, createDb, parseCursorParams, parseOffsetParams };
|
|
657
|
+
export { type CacheBackend, type CacheConfig, type CursorPage, type CursorParams, type Db, type DbConfig, type DeleteBuilder, type DeleteReturningBuilder, type FindFirstOptions, type FindManyOptions, type InsertBuilder, type InsertReturningBuilder, type OffsetPage, type OffsetParams, type Operation, type PaginateOptions, type PaginateParams, type QueryBuilder, type QueryCacheOptions, type UpdateBuilder, type UpdateReturningBuilder, type UpdateWhereBuilder, compose, composeSequential, createDb, parseCursorParams, parseOffsetParams };
|
package/dist/index.js
CHANGED
|
@@ -583,9 +583,10 @@ function buildDb(config, isUnsafe) {
|
|
|
583
583
|
return {
|
|
584
584
|
permissions: allPermissions,
|
|
585
585
|
async run(params) {
|
|
586
|
+
const p = params ?? {};
|
|
586
587
|
const results = [];
|
|
587
588
|
for (const op of operations) {
|
|
588
|
-
results.push(await op.run(
|
|
589
|
+
results.push(await op.run(p));
|
|
589
590
|
}
|
|
590
591
|
return results;
|
|
591
592
|
}
|
|
@@ -620,8 +621,24 @@ function compose(operations, executor) {
|
|
|
620
621
|
}
|
|
621
622
|
};
|
|
622
623
|
}
|
|
624
|
+
function composeSequential(operations) {
|
|
625
|
+
const allPermissions = deduplicateDescriptors(
|
|
626
|
+
operations.flatMap((op) => op.permissions)
|
|
627
|
+
);
|
|
628
|
+
return {
|
|
629
|
+
permissions: allPermissions,
|
|
630
|
+
async run() {
|
|
631
|
+
const results = [];
|
|
632
|
+
for (const op of operations) {
|
|
633
|
+
results.push(await op.run({}));
|
|
634
|
+
}
|
|
635
|
+
return results;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
623
639
|
export {
|
|
624
640
|
compose,
|
|
641
|
+
composeSequential,
|
|
625
642
|
createDb,
|
|
626
643
|
parseCursorParams,
|
|
627
644
|
parseOffsetParams
|
package/llms.txt
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @cfast/db
|
|
2
|
+
|
|
3
|
+
> Lazy, permission-aware Drizzle operations for Cloudflare D1 -- application-level Row-Level Security.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
Use `@cfast/db` whenever you need to read or write a D1 database in a cfast app. Every query goes through permission checks automatically. You never call Drizzle directly.
|
|
8
|
+
|
|
9
|
+
## Key concepts
|
|
10
|
+
|
|
11
|
+
- **Operations are lazy.** Every `db.query/insert/update/delete` call returns an `Operation<TResult>` with `.permissions` (inspectable immediately) and `.run(params)` (executes with permission checks). Nothing touches D1 until you call `.run()`.
|
|
12
|
+
- **Permission checks are structural.** `.run()` always checks the user's grants before executing SQL. Row-level WHERE clauses from grants are injected automatically.
|
|
13
|
+
- **One Db per request.** `createDb()` captures the user at creation time. Never share a `Db` across requests.
|
|
14
|
+
- **`db.unsafe()` is the only escape hatch.** Returns a `Db` that skips all permission checks. Greppable via `git grep '.unsafe()'`.
|
|
15
|
+
|
|
16
|
+
## API Reference
|
|
17
|
+
|
|
18
|
+
### createDb(config): Db
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createDb } from "@cfast/db";
|
|
22
|
+
import * as schema from "./schema"; // must be `import *`
|
|
23
|
+
|
|
24
|
+
const db = createDb({
|
|
25
|
+
d1: D1Database, // env.DB
|
|
26
|
+
schema: Record<string, DrizzleTable>,
|
|
27
|
+
grants: Grant[], // from resolveGrants(permissions, roles)
|
|
28
|
+
user: { id: string } | null, // null = anonymous
|
|
29
|
+
cache?: CacheConfig | false, // default: { backend: "cache-api" }
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Operation<TResult>
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
type Operation<TResult> = {
|
|
37
|
+
permissions: PermissionDescriptor[]; // [{ action, table }] -- available immediately
|
|
38
|
+
run: (params?: Record<string, unknown>) => Promise<TResult>;
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Reads
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
db.query(table).findMany(options?): Operation<unknown[]>
|
|
46
|
+
db.query(table).findFirst(options?): Operation<unknown | undefined>
|
|
47
|
+
db.query(table).paginate(params, options?): Operation<CursorPage | OffsetPage>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
FindManyOptions: `{ columns?, where?, orderBy?, limit?, offset?, with?, cache? }`
|
|
51
|
+
FindFirstOptions: same without `limit`/`offset`.
|
|
52
|
+
|
|
53
|
+
### Writes
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
db.insert(table).values({...}): InsertReturningBuilder // .run() or .returning().run()
|
|
57
|
+
db.update(table).set({...}).where(cond): UpdateReturningBuilder
|
|
58
|
+
db.delete(table).where(cond): DeleteReturningBuilder
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
All write builders are `Operation<void>` with an optional `.returning()` that changes return type.
|
|
62
|
+
|
|
63
|
+
### compose(operations, executor): Operation<TResult>
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { compose } from "@cfast/db";
|
|
67
|
+
|
|
68
|
+
const op = compose(
|
|
69
|
+
[updatePost, insertAuditLog],
|
|
70
|
+
async (doUpdate, doAudit) => {
|
|
71
|
+
const result = await doUpdate();
|
|
72
|
+
await doAudit();
|
|
73
|
+
return result;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
op.permissions; // merged + deduplicated from both operations
|
|
77
|
+
await op.run(); // executor runs, each sub-op checks its own permissions
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### composeSequential(operations): Operation<unknown[]>
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { composeSequential } from "@cfast/db";
|
|
84
|
+
|
|
85
|
+
const op = composeSequential([updatePost, insertAuditLog]);
|
|
86
|
+
op.permissions; // merged + deduplicated from all operations
|
|
87
|
+
await op.run(); // runs operations in order, returns results array
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Runs operations in order, returns results array. Shorthand for `compose` when there are no data dependencies between operations.
|
|
91
|
+
|
|
92
|
+
### db.batch(operations): Operation<unknown[]>
|
|
93
|
+
|
|
94
|
+
Runs operations sequentially with merged permissions. Not D1 native batch.
|
|
95
|
+
|
|
96
|
+
### db.unsafe(): Db
|
|
97
|
+
|
|
98
|
+
Returns a new Db that skips permission checks. Use for cron jobs, migrations, system tasks.
|
|
99
|
+
|
|
100
|
+
### Pagination helpers
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { parseCursorParams, parseOffsetParams } from "@cfast/db";
|
|
104
|
+
|
|
105
|
+
const params = parseCursorParams(request, { defaultLimit: 20, maxLimit: 100 });
|
|
106
|
+
const page = await db.query(posts).paginate(params, {
|
|
107
|
+
cursorColumns: [posts.createdAt, posts.id],
|
|
108
|
+
orderDirection: "desc",
|
|
109
|
+
}).run();
|
|
110
|
+
// CursorPage: { items, nextCursor }
|
|
111
|
+
|
|
112
|
+
const offsetParams = parseOffsetParams(request);
|
|
113
|
+
const page2 = await db.query(posts).paginate(offsetParams).run();
|
|
114
|
+
// OffsetPage: { items, total, page, totalPages }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Cache
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
cache: {
|
|
121
|
+
backend: "cache-api" | "kv",
|
|
122
|
+
ttl?: "30s",
|
|
123
|
+
staleWhileRevalidate?: "5m",
|
|
124
|
+
exclude?: ["sessions"],
|
|
125
|
+
kv?: KVNamespace, // required for "kv" backend
|
|
126
|
+
onHit?, onMiss?, onInvalidate?,
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Per-query: `db.query(t).findMany({ cache: false })` or `{ cache: { ttl: "5m", tags: ["posts"] } }`.
|
|
131
|
+
Manual invalidation: `await db.cache.invalidate({ tags: ["posts"], tables: ["posts"] })`.
|
|
132
|
+
|
|
133
|
+
## Usage Examples
|
|
134
|
+
|
|
135
|
+
### Standard loader pattern
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
export async function loader({ context, request }) {
|
|
139
|
+
const { user, grants } = await auth.requireUser(request);
|
|
140
|
+
const db = createDb({ d1: context.env.DB, schema, grants, user });
|
|
141
|
+
|
|
142
|
+
const posts = await db.query(postsTable).findMany({
|
|
143
|
+
where: eq(postsTable.published, true),
|
|
144
|
+
orderBy: desc(postsTable.createdAt),
|
|
145
|
+
limit: 10,
|
|
146
|
+
}).run();
|
|
147
|
+
|
|
148
|
+
return { posts };
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Compose a multi-step action
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
export async function action({ context, request }) {
|
|
156
|
+
const { user, grants } = await auth.requireUser(request);
|
|
157
|
+
const db = createDb({ d1: context.env.DB, schema, grants, user });
|
|
158
|
+
|
|
159
|
+
const workflow = compose(
|
|
160
|
+
[
|
|
161
|
+
db.update(posts).set({ published: true }).where(eq(posts.id, id)),
|
|
162
|
+
db.insert(auditLogs).values({ action: "publish", targetId: id, userId: user.id }),
|
|
163
|
+
],
|
|
164
|
+
async (doUpdate, doAudit) => {
|
|
165
|
+
await doUpdate();
|
|
166
|
+
await doAudit();
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
await workflow.run();
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Integration
|
|
175
|
+
|
|
176
|
+
- **@cfast/permissions** -- `grants` come from `resolveGrants(permissions, user.roles)`. Permission WHERE clauses are defined via `grant()` in your permissions config.
|
|
177
|
+
- **@cfast/auth** -- `auth.requireUser(request)` returns `{ user, grants }` which you pass directly to `createDb()`.
|
|
178
|
+
- **@cfast/actions** -- Actions define operations using `@cfast/db`. The framework extracts `.permissions` for client-side UI adaptation.
|
|
179
|
+
|
|
180
|
+
## Common Mistakes
|
|
181
|
+
|
|
182
|
+
- **Forgetting `.run()`** -- Operations are lazy. `db.query(t).findMany()` returns an Operation, not results. You must call `.run()`.
|
|
183
|
+
- **Sharing a Db across requests** -- The Db captures the user. Create a new one per request.
|
|
184
|
+
- **Using `import { posts }` instead of `import * as schema`** -- The `schema` config must be `import *` so Drizzle's relational API can look up tables by key name.
|
|
185
|
+
- **Using `db.unsafe()` for admin endpoints** -- Use a role with `grant("manage", "all")` instead. Reserve `unsafe()` for genuinely user-less contexts (cron, migrations).
|
|
186
|
+
- **Expecting relational `with` to have permission filters** -- Permission WHERE clauses only apply to the root table, not joined relations.
|
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfast/db",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Permission-aware Drizzle queries for Cloudflare D1",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cfast",
|
|
7
|
+
"cloudflare-workers",
|
|
8
|
+
"drizzle",
|
|
9
|
+
"d1",
|
|
10
|
+
"permissions",
|
|
11
|
+
"database"
|
|
12
|
+
],
|
|
5
13
|
"license": "MIT",
|
|
6
14
|
"repository": {
|
|
7
15
|
"type": "git",
|
|
@@ -18,17 +26,25 @@
|
|
|
18
26
|
}
|
|
19
27
|
},
|
|
20
28
|
"files": [
|
|
21
|
-
"dist"
|
|
29
|
+
"dist",
|
|
30
|
+
"llms.txt"
|
|
22
31
|
],
|
|
23
32
|
"sideEffects": false,
|
|
24
33
|
"publishConfig": {
|
|
25
34
|
"access": "public"
|
|
26
35
|
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
38
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"lint": "eslint src/",
|
|
41
|
+
"test": "vitest run"
|
|
42
|
+
},
|
|
27
43
|
"peerDependencies": {
|
|
28
44
|
"drizzle-orm": ">=0.35"
|
|
29
45
|
},
|
|
30
46
|
"dependencies": {
|
|
31
|
-
"@cfast/permissions": "
|
|
47
|
+
"@cfast/permissions": "workspace:*"
|
|
32
48
|
},
|
|
33
49
|
"peerDependenciesMeta": {
|
|
34
50
|
"@cloudflare/workers-types": {
|
|
@@ -41,12 +57,5 @@
|
|
|
41
57
|
"tsup": "^8",
|
|
42
58
|
"typescript": "^5.7",
|
|
43
59
|
"vitest": "^4.1.0"
|
|
44
|
-
},
|
|
45
|
-
"scripts": {
|
|
46
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
47
|
-
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
48
|
-
"typecheck": "tsc --noEmit",
|
|
49
|
-
"lint": "eslint src/",
|
|
50
|
-
"test": "vitest run"
|
|
51
60
|
}
|
|
52
|
-
}
|
|
61
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Daniel Schmidt
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|