@cfast/db 0.1.0 → 0.2.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/LICENSE +21 -0
- package/dist/index.d.ts +127 -32
- package/dist/index.js +220 -38
- package/llms.txt +111 -2
- package/package.json +10 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { DrizzleTable, PermissionDescriptor, Grant } from '@cfast/permissions';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Extracts the row type from a Drizzle table reference.
|
|
5
|
+
*
|
|
6
|
+
* Drizzle tables expose `$inferSelect` (the row shape returned by `SELECT *`).
|
|
7
|
+
* `InferRow<typeof posts>` resolves to `{ id: string; title: string; ... }`.
|
|
8
|
+
*
|
|
9
|
+
* Falls back to `Record<string, unknown>` for opaque `DrizzleTable` references
|
|
10
|
+
* (e.g. when callers do not specify a concrete table type generic).
|
|
11
|
+
*
|
|
12
|
+
* @typeParam TTable - The Drizzle table type (e.g. `typeof posts`).
|
|
13
|
+
*/
|
|
14
|
+
type InferRow<TTable> = TTable extends {
|
|
15
|
+
$inferSelect: infer R;
|
|
16
|
+
} ? R : Record<string, unknown>;
|
|
3
17
|
/**
|
|
4
18
|
* A lazy, permission-aware database operation.
|
|
5
19
|
*
|
|
@@ -126,8 +140,13 @@ type DbConfig = {
|
|
|
126
140
|
/**
|
|
127
141
|
* Drizzle schema object. Must be `import * as schema` so that keys match
|
|
128
142
|
* table variable names (required by Drizzle's relational query API).
|
|
143
|
+
*
|
|
144
|
+
* Typed as `Record<string, unknown>` so callers can pass `import * as schema`
|
|
145
|
+
* directly without casting -- Drizzle schemas typically include `Relations`
|
|
146
|
+
* exports alongside tables, and the `@cfast/db` runtime ignores any non-table
|
|
147
|
+
* entries when looking up tables by key.
|
|
129
148
|
*/
|
|
130
|
-
schema: Record<string,
|
|
149
|
+
schema: Record<string, unknown>;
|
|
131
150
|
/** Resolved permission grants for the current user's role, from `resolveGrants()`. */
|
|
132
151
|
grants: Grant[];
|
|
133
152
|
/**
|
|
@@ -348,14 +367,19 @@ type PaginateOptions = {
|
|
|
348
367
|
* ```
|
|
349
368
|
*/
|
|
350
369
|
type Db = {
|
|
351
|
-
/**
|
|
352
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Creates a {@link QueryBuilder} for reading rows from the given table.
|
|
372
|
+
*
|
|
373
|
+
* The builder is generic over `TTable`, so `findMany`/`findFirst` return rows
|
|
374
|
+
* typed via `InferRow<TTable>` -- callers don't need to cast to `as any`.
|
|
375
|
+
*/
|
|
376
|
+
query: <TTable extends DrizzleTable>(table: TTable) => QueryBuilder<TTable>;
|
|
353
377
|
/** Creates an {@link InsertBuilder} for inserting rows into the given table. */
|
|
354
|
-
insert: (table:
|
|
378
|
+
insert: <TTable extends DrizzleTable>(table: TTable) => InsertBuilder<TTable>;
|
|
355
379
|
/** Creates an {@link UpdateBuilder} for updating rows in the given table. */
|
|
356
|
-
update: (table:
|
|
380
|
+
update: <TTable extends DrizzleTable>(table: TTable) => UpdateBuilder<TTable>;
|
|
357
381
|
/** Creates a {@link DeleteBuilder} for deleting rows from the given table. */
|
|
358
|
-
delete: (table:
|
|
382
|
+
delete: <TTable extends DrizzleTable>(table: TTable) => DeleteBuilder<TTable>;
|
|
359
383
|
/**
|
|
360
384
|
* Returns a new `Db` instance that skips all permission checks.
|
|
361
385
|
*
|
|
@@ -365,7 +389,20 @@ type Db = {
|
|
|
365
389
|
unsafe: () => Db;
|
|
366
390
|
/**
|
|
367
391
|
* Groups multiple operations into a single {@link Operation} with merged, deduplicated permissions.
|
|
368
|
-
*
|
|
392
|
+
*
|
|
393
|
+
* When every operation was produced by `db.insert/update/delete`, the batch is
|
|
394
|
+
* executed via D1's native `batch()` API, which is **atomic** -- if any
|
|
395
|
+
* statement fails, the entire batch is rolled back. This is the recommended
|
|
396
|
+
* way to perform multi-step mutations that need transactional safety, such as
|
|
397
|
+
* decrementing stock across multiple products during checkout.
|
|
398
|
+
*
|
|
399
|
+
* Permissions for every sub-operation are checked **upfront**: if the user
|
|
400
|
+
* lacks any required grant, the batch throws before any SQL is issued.
|
|
401
|
+
*
|
|
402
|
+
* Operations that don't carry the internal batchable hook (for example, ops
|
|
403
|
+
* produced by `compose()` executors) cause the batch to fall back to
|
|
404
|
+
* sequential execution. This preserves backward compatibility for non-trivial
|
|
405
|
+
* compositions but loses the atomicity guarantee.
|
|
369
406
|
*/
|
|
370
407
|
batch: (operations: Operation<unknown>[]) => Operation<unknown[]>;
|
|
371
408
|
/** Cache control methods for manual invalidation. */
|
|
@@ -399,18 +436,26 @@ type Db = {
|
|
|
399
436
|
* const page = await builder.paginate(params, { orderBy: desc(posts.createdAt) }).run({});
|
|
400
437
|
* ```
|
|
401
438
|
*/
|
|
402
|
-
type QueryBuilder = {
|
|
403
|
-
/**
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
439
|
+
type QueryBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
440
|
+
/**
|
|
441
|
+
* Returns an {@link Operation} that fetches multiple rows matching the given options.
|
|
442
|
+
*
|
|
443
|
+
* The result is typed via `InferRow<TTable>`, so callers get IntelliSense on
|
|
444
|
+
* `(row) => row.title` without any cast.
|
|
445
|
+
*/
|
|
446
|
+
findMany: (options?: FindManyOptions) => Operation<InferRow<TTable>[]>;
|
|
447
|
+
/**
|
|
448
|
+
* Returns an {@link Operation} that fetches the first matching row, or `undefined`
|
|
449
|
+
* if none match. The row type is propagated from `TTable`.
|
|
450
|
+
*/
|
|
451
|
+
findFirst: (options?: FindFirstOptions) => Operation<InferRow<TTable> | undefined>;
|
|
407
452
|
/**
|
|
408
453
|
* Returns a paginated {@link Operation} using either cursor-based or offset-based strategy.
|
|
409
454
|
*
|
|
410
455
|
* The return type depends on the `params.type` discriminant: {@link CursorPage} for `"cursor"`,
|
|
411
|
-
* {@link OffsetPage} for `"offset"`.
|
|
456
|
+
* {@link OffsetPage} for `"offset"`. Each page's items are typed as `InferRow<TTable>`.
|
|
412
457
|
*/
|
|
413
|
-
paginate: (params: CursorParams | OffsetParams, options?: PaginateOptions) => Operation<CursorPage<
|
|
458
|
+
paginate: (params: CursorParams | OffsetParams, options?: PaginateOptions) => Operation<CursorPage<InferRow<TTable>>> | Operation<OffsetPage<InferRow<TTable>>>;
|
|
414
459
|
};
|
|
415
460
|
/**
|
|
416
461
|
* Builder for insert operations on a single table.
|
|
@@ -430,19 +475,19 @@ type QueryBuilder = {
|
|
|
430
475
|
* .run({});
|
|
431
476
|
* ```
|
|
432
477
|
*/
|
|
433
|
-
type InsertBuilder = {
|
|
478
|
+
type InsertBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
434
479
|
/** Specifies the column values to insert, returning an {@link InsertReturningBuilder}. */
|
|
435
|
-
values: (values: Record<string, unknown>) => InsertReturningBuilder
|
|
480
|
+
values: (values: Record<string, unknown>) => InsertReturningBuilder<TTable>;
|
|
436
481
|
};
|
|
437
482
|
/**
|
|
438
483
|
* An insert {@link Operation} that optionally returns the inserted row via `.returning()`.
|
|
439
484
|
*
|
|
440
485
|
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
441
|
-
* it resolves to the full inserted row
|
|
486
|
+
* it resolves to the full inserted row, typed as `InferRow<TTable>`.
|
|
442
487
|
*/
|
|
443
|
-
type InsertReturningBuilder = Operation<void> & {
|
|
488
|
+
type InsertReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
444
489
|
/** Chains `.returning()` to get the inserted row back from D1. */
|
|
445
|
-
returning: () => Operation<
|
|
490
|
+
returning: () => Operation<InferRow<TTable>>;
|
|
446
491
|
};
|
|
447
492
|
/**
|
|
448
493
|
* Builder for update operations on a single table.
|
|
@@ -458,28 +503,28 @@ type InsertReturningBuilder = Operation<void> & {
|
|
|
458
503
|
* .run({});
|
|
459
504
|
* ```
|
|
460
505
|
*/
|
|
461
|
-
type UpdateBuilder = {
|
|
506
|
+
type UpdateBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
462
507
|
/** Specifies the column values to update, returning an {@link UpdateWhereBuilder}. */
|
|
463
|
-
set: (values: Record<string, unknown>) => UpdateWhereBuilder
|
|
508
|
+
set: (values: Record<string, unknown>) => UpdateWhereBuilder<TTable>;
|
|
464
509
|
};
|
|
465
510
|
/**
|
|
466
511
|
* Intermediate builder requiring a WHERE condition before the update can execute.
|
|
467
512
|
*
|
|
468
513
|
* The WHERE condition is AND'd with any permission-based WHERE clauses from the user's grants.
|
|
469
514
|
*/
|
|
470
|
-
type UpdateWhereBuilder = {
|
|
515
|
+
type UpdateWhereBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
471
516
|
/** Specifies the WHERE condition (AND'd with permission filters at `.run()` time). */
|
|
472
|
-
where: (condition: unknown) => UpdateReturningBuilder
|
|
517
|
+
where: (condition: unknown) => UpdateReturningBuilder<TTable>;
|
|
473
518
|
};
|
|
474
519
|
/**
|
|
475
520
|
* An update {@link Operation} that optionally returns the updated row via `.returning()`.
|
|
476
521
|
*
|
|
477
522
|
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
478
|
-
* it resolves to the full updated row
|
|
523
|
+
* it resolves to the full updated row, typed as `InferRow<TTable>`.
|
|
479
524
|
*/
|
|
480
|
-
type UpdateReturningBuilder = Operation<void> & {
|
|
525
|
+
type UpdateReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
481
526
|
/** Chains `.returning()` to get the updated row back from D1. */
|
|
482
|
-
returning: () => Operation<
|
|
527
|
+
returning: () => Operation<InferRow<TTable>>;
|
|
483
528
|
};
|
|
484
529
|
/**
|
|
485
530
|
* Builder for delete operations on a single table.
|
|
@@ -492,19 +537,19 @@ type UpdateReturningBuilder = Operation<void> & {
|
|
|
492
537
|
* await db.delete(posts).where(eq(posts.id, "abc-123")).run({});
|
|
493
538
|
* ```
|
|
494
539
|
*/
|
|
495
|
-
type DeleteBuilder = {
|
|
540
|
+
type DeleteBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
496
541
|
/** Specifies the WHERE condition (AND'd with permission filters at `.run()` time). */
|
|
497
|
-
where: (condition: unknown) => DeleteReturningBuilder
|
|
542
|
+
where: (condition: unknown) => DeleteReturningBuilder<TTable>;
|
|
498
543
|
};
|
|
499
544
|
/**
|
|
500
545
|
* A delete {@link Operation} that optionally returns the deleted row via `.returning()`.
|
|
501
546
|
*
|
|
502
547
|
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
503
|
-
* it resolves to the full deleted row
|
|
548
|
+
* it resolves to the full deleted row, typed as `InferRow<TTable>`.
|
|
504
549
|
*/
|
|
505
|
-
type DeleteReturningBuilder = Operation<void> & {
|
|
550
|
+
type DeleteReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
506
551
|
/** Chains `.returning()` to get the deleted row back from D1. */
|
|
507
|
-
returning: () => Operation<
|
|
552
|
+
returning: () => Operation<InferRow<TTable>>;
|
|
508
553
|
};
|
|
509
554
|
|
|
510
555
|
/**
|
|
@@ -600,6 +645,56 @@ declare function compose<TResult>(operations: Operation<unknown>[], executor: (.
|
|
|
600
645
|
* @returns A single {@link Operation} that runs all operations sequentially and returns their results.
|
|
601
646
|
*/
|
|
602
647
|
declare function composeSequential(operations: Operation<unknown>[]): Operation<unknown[]>;
|
|
648
|
+
/**
|
|
649
|
+
* Sequential `compose` variant that accepts a callback for ad-hoc workflows where
|
|
650
|
+
* later operations depend on the results of earlier ones (e.g. inserting a child
|
|
651
|
+
* row that references the inserted parent's id).
|
|
652
|
+
*
|
|
653
|
+
* Permissions are still collected ahead-of-time: the callback is run twice. The
|
|
654
|
+
* first pass uses a tracking proxy that records every operation's permission
|
|
655
|
+
* descriptors and returns sentinel values from `.run()` (so `card.id` and similar
|
|
656
|
+
* accesses don't crash). The second pass runs the callback against the real Db
|
|
657
|
+
* to actually execute the queries.
|
|
658
|
+
*
|
|
659
|
+
* Because the callback runs twice, **it must be free of side effects outside of
|
|
660
|
+
* `db.*` calls** (no `fetch`, no `console.log` you care about, no global state
|
|
661
|
+
* mutation). All control flow that depends on db results should use the sentinel-
|
|
662
|
+
* tolerant property accesses.
|
|
663
|
+
*
|
|
664
|
+
* The dry-run pass is asynchronous, so `op.permissions` is empty until the dry-run
|
|
665
|
+
* resolves. Consumers that need to inspect permissions before calling `.run()` (e.g.
|
|
666
|
+
* the `@cfast/actions` permission UI) should `await op.permissionsAsync()` instead.
|
|
667
|
+
* Calling `op.run()` always awaits the dry-run internally, so permissions are
|
|
668
|
+
* guaranteed to be populated by the time the real callback runs.
|
|
669
|
+
*
|
|
670
|
+
* @typeParam TResult - The return type of the callback.
|
|
671
|
+
* @param db - The real Db that will execute the operations on the second pass.
|
|
672
|
+
* @param callback - A function that receives a Db and returns a value.
|
|
673
|
+
* @returns A single {@link Operation} whose `.permissions` is the union of every
|
|
674
|
+
* operation discovered during the dry-run pass and whose `.run()` re-executes
|
|
675
|
+
* the callback against the real Db. The returned object also exposes
|
|
676
|
+
* `permissionsAsync()` for guaranteed permission retrieval.
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```ts
|
|
680
|
+
* import { composeSequentialCallback } from "@cfast/db";
|
|
681
|
+
*
|
|
682
|
+
* const op = composeSequentialCallback(db, async (tx) => {
|
|
683
|
+
* const card = await tx.insert(cards).values({ title: "New" }).returning().run();
|
|
684
|
+
* await tx.insert(activityLog).values({
|
|
685
|
+
* cardId: (card as { id: string }).id,
|
|
686
|
+
* action: "card_created",
|
|
687
|
+
* }).run();
|
|
688
|
+
* return card;
|
|
689
|
+
* });
|
|
690
|
+
*
|
|
691
|
+
* await op.permissionsAsync(); // [{ create, cards }, { create, activityLog }]
|
|
692
|
+
* await op.run();
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
declare function composeSequentialCallback<TResult>(db: Db, callback: (db: Db) => Promise<TResult>): Operation<TResult> & {
|
|
696
|
+
permissionsAsync: () => Promise<PermissionDescriptor[]>;
|
|
697
|
+
};
|
|
603
698
|
|
|
604
699
|
/**
|
|
605
700
|
* Options for controlling default and maximum pagination limits.
|
|
@@ -654,4 +749,4 @@ declare function parseCursorParams(request: Request, options?: PaginationOptions
|
|
|
654
749
|
*/
|
|
655
750
|
declare function parseOffsetParams(request: Request, options?: PaginationOptions): OffsetParams;
|
|
656
751
|
|
|
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 };
|
|
752
|
+
export { type CacheBackend, type CacheConfig, type CursorPage, type CursorParams, type Db, type DbConfig, type DeleteBuilder, type DeleteReturningBuilder, type FindFirstOptions, type FindManyOptions, type InferRow, 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, composeSequentialCallback, createDb, parseCursorParams, parseOffsetParams };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/create-db.ts
|
|
2
|
+
import { drizzle as drizzle3 } from "drizzle-orm/d1";
|
|
3
|
+
|
|
1
4
|
// src/query-builder.ts
|
|
2
5
|
import { count } from "drizzle-orm";
|
|
3
6
|
import { drizzle } from "drizzle-orm/d1";
|
|
@@ -92,7 +95,8 @@ function makePermissions(unsafe, action, table) {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
// src/paginate.ts
|
|
95
|
-
import { and as and2, or as or2, lt, gt, eq } from "drizzle-orm";
|
|
98
|
+
import { and as and2, or as or2, lt, gt, eq, getTableColumns } from "drizzle-orm";
|
|
99
|
+
import { getTableConfig } from "drizzle-orm/sqlite-core";
|
|
96
100
|
function parseIntParam(raw, fallback) {
|
|
97
101
|
if (raw == null) return fallback;
|
|
98
102
|
const n = Number(raw);
|
|
@@ -135,6 +139,23 @@ function decodeCursor(cursor) {
|
|
|
135
139
|
return null;
|
|
136
140
|
}
|
|
137
141
|
}
|
|
142
|
+
function getPrimaryKeyColumns(table) {
|
|
143
|
+
const tableColumns = getTableColumns(table);
|
|
144
|
+
const singleColumnPks = [];
|
|
145
|
+
for (const col of Object.values(tableColumns)) {
|
|
146
|
+
if (col.primary) {
|
|
147
|
+
singleColumnPks.push(col);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (singleColumnPks.length > 0) {
|
|
151
|
+
return singleColumnPks;
|
|
152
|
+
}
|
|
153
|
+
const config = getTableConfig(table);
|
|
154
|
+
if (config.primaryKeys.length > 0) {
|
|
155
|
+
return config.primaryKeys[0].columns;
|
|
156
|
+
}
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
138
159
|
function buildCursorWhere(cursorColumns, cursorValues, direction = "desc") {
|
|
139
160
|
const compare = direction === "desc" ? lt : gt;
|
|
140
161
|
if (cursorColumns.length === 1) {
|
|
@@ -247,7 +268,13 @@ function createQueryBuilder(config) {
|
|
|
247
268
|
return qo;
|
|
248
269
|
}
|
|
249
270
|
if (params.type === "cursor") {
|
|
250
|
-
const
|
|
271
|
+
const explicitCursorColumns = options?.cursorColumns;
|
|
272
|
+
const cursorColumns = explicitCursorColumns && explicitCursorColumns.length > 0 ? explicitCursorColumns : getPrimaryKeyColumns(config.table);
|
|
273
|
+
if (cursorColumns.length === 0) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"paginate(): cursor pagination requires either explicit `cursorColumns` or a table with a primary key. Pass `cursorColumns` in the paginate options."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
251
278
|
return {
|
|
252
279
|
permissions,
|
|
253
280
|
async run(_params) {
|
|
@@ -302,50 +329,80 @@ function createQueryBuilder(config) {
|
|
|
302
329
|
|
|
303
330
|
// src/mutate-builder.ts
|
|
304
331
|
import { drizzle as drizzle2 } from "drizzle-orm/d1";
|
|
332
|
+
|
|
333
|
+
// src/batchable.ts
|
|
334
|
+
var BATCHABLE = /* @__PURE__ */ Symbol.for("@cfast/db/batchable");
|
|
335
|
+
function getBatchable(op) {
|
|
336
|
+
if (op === null || typeof op !== "object") return void 0;
|
|
337
|
+
const meta = op[BATCHABLE];
|
|
338
|
+
return meta;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/mutate-builder.ts
|
|
305
342
|
function checkIfNeeded(config, grants, permissions) {
|
|
306
343
|
if (!config.unsafe) {
|
|
307
344
|
checkOperationPermissions(grants, permissions);
|
|
308
345
|
}
|
|
309
346
|
}
|
|
310
|
-
function buildMutationWithReturning(config, permissions, tableName,
|
|
311
|
-
|
|
347
|
+
function buildMutationWithReturning(config, permissions, tableName, buildQuery) {
|
|
348
|
+
const drizzleDb = drizzle2(config.d1, { schema: config.schema });
|
|
349
|
+
const runOnce = async (returning) => {
|
|
350
|
+
checkIfNeeded(config, config.grants, permissions);
|
|
351
|
+
const built = buildQuery(drizzleDb, returning);
|
|
352
|
+
const result = await built.execute(returning);
|
|
353
|
+
config.onMutate?.(tableName);
|
|
354
|
+
return result;
|
|
355
|
+
};
|
|
356
|
+
const baseOp = {
|
|
312
357
|
permissions,
|
|
313
358
|
async run(_params) {
|
|
314
|
-
|
|
315
|
-
await execute(false);
|
|
316
|
-
config.onMutate?.(tableName);
|
|
359
|
+
await runOnce(false);
|
|
317
360
|
},
|
|
318
361
|
returning() {
|
|
319
|
-
|
|
362
|
+
const returningOp = {
|
|
320
363
|
permissions,
|
|
321
364
|
async run(_params) {
|
|
322
|
-
|
|
323
|
-
const result = await execute(true);
|
|
324
|
-
config.onMutate?.(tableName);
|
|
325
|
-
return result;
|
|
365
|
+
return runOnce(true);
|
|
326
366
|
}
|
|
327
367
|
};
|
|
368
|
+
returningOp[BATCHABLE] = {
|
|
369
|
+
build: (sharedDb) => buildQuery(sharedDb, true).query,
|
|
370
|
+
tableName,
|
|
371
|
+
withResult: true
|
|
372
|
+
};
|
|
373
|
+
return returningOp;
|
|
328
374
|
}
|
|
329
375
|
};
|
|
376
|
+
baseOp[BATCHABLE] = {
|
|
377
|
+
build: (sharedDb) => buildQuery(sharedDb, false).query,
|
|
378
|
+
tableName,
|
|
379
|
+
withResult: false
|
|
380
|
+
};
|
|
381
|
+
return baseOp;
|
|
330
382
|
}
|
|
331
383
|
function createInsertBuilder(config) {
|
|
332
|
-
const db = drizzle2(config.d1, { schema: config.schema });
|
|
333
384
|
const permissions = makePermissions(config.unsafe, "create", config.table);
|
|
334
385
|
const tableName = getTableName2(config.table);
|
|
335
386
|
return {
|
|
336
387
|
values(values) {
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
388
|
+
const buildQuery = (sharedDb, returning) => {
|
|
389
|
+
const base = sharedDb.insert(config.table).values(values);
|
|
390
|
+
const query = returning ? base.returning() : base;
|
|
391
|
+
return {
|
|
392
|
+
query,
|
|
393
|
+
execute: async (wantsResult) => {
|
|
394
|
+
if (wantsResult) {
|
|
395
|
+
return query.get();
|
|
396
|
+
}
|
|
397
|
+
await base.run();
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
return buildMutationWithReturning(config, permissions, tableName, buildQuery);
|
|
344
402
|
}
|
|
345
403
|
};
|
|
346
404
|
}
|
|
347
405
|
function createUpdateBuilder(config) {
|
|
348
|
-
const db = drizzle2(config.d1, { schema: config.schema });
|
|
349
406
|
const permissions = makePermissions(config.unsafe, "update", config.table);
|
|
350
407
|
const tableName = getTableName2(config.table);
|
|
351
408
|
return {
|
|
@@ -360,21 +417,27 @@ function createUpdateBuilder(config) {
|
|
|
360
417
|
config.unsafe
|
|
361
418
|
);
|
|
362
419
|
const combinedWhere = combineWhere(condition, permFilter);
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
if (combinedWhere)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
420
|
+
const buildQuery = (sharedDb, returning) => {
|
|
421
|
+
const base = sharedDb.update(config.table).set(values);
|
|
422
|
+
if (combinedWhere) base.where(combinedWhere);
|
|
423
|
+
const query = returning ? base.returning() : base;
|
|
424
|
+
return {
|
|
425
|
+
query,
|
|
426
|
+
execute: async (wantsResult) => {
|
|
427
|
+
if (wantsResult) {
|
|
428
|
+
return query.get();
|
|
429
|
+
}
|
|
430
|
+
await base.run();
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
return buildMutationWithReturning(config, permissions, tableName, buildQuery);
|
|
371
435
|
}
|
|
372
436
|
};
|
|
373
437
|
}
|
|
374
438
|
};
|
|
375
439
|
}
|
|
376
440
|
function createDeleteBuilder(config) {
|
|
377
|
-
const db = drizzle2(config.d1, { schema: config.schema });
|
|
378
441
|
const permissions = makePermissions(config.unsafe, "delete", config.table);
|
|
379
442
|
const tableName = getTableName2(config.table);
|
|
380
443
|
return {
|
|
@@ -387,14 +450,21 @@ function createDeleteBuilder(config) {
|
|
|
387
450
|
config.unsafe
|
|
388
451
|
);
|
|
389
452
|
const combinedWhere = combineWhere(condition, permFilter);
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
if (combinedWhere)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
453
|
+
const buildQuery = (sharedDb, returning) => {
|
|
454
|
+
const base = sharedDb.delete(config.table);
|
|
455
|
+
if (combinedWhere) base.where(combinedWhere);
|
|
456
|
+
const query = returning ? base.returning() : base;
|
|
457
|
+
return {
|
|
458
|
+
query,
|
|
459
|
+
execute: async (wantsResult) => {
|
|
460
|
+
if (wantsResult) {
|
|
461
|
+
return query.get();
|
|
462
|
+
}
|
|
463
|
+
await base.run();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
return buildMutationWithReturning(config, permissions, tableName, buildQuery);
|
|
398
468
|
}
|
|
399
469
|
};
|
|
400
470
|
}
|
|
@@ -584,6 +654,26 @@ function buildDb(config, isUnsafe) {
|
|
|
584
654
|
permissions: allPermissions,
|
|
585
655
|
async run(params) {
|
|
586
656
|
const p = params ?? {};
|
|
657
|
+
if (!isUnsafe) {
|
|
658
|
+
checkOperationPermissions(config.grants, allPermissions);
|
|
659
|
+
}
|
|
660
|
+
const batchables = operations.map((op) => getBatchable(op));
|
|
661
|
+
const everyOpBatchable = operations.length > 0 && batchables.every((b) => b !== void 0);
|
|
662
|
+
if (everyOpBatchable) {
|
|
663
|
+
const sharedDb = drizzle3(config.d1, { schema: config.schema });
|
|
664
|
+
const items = batchables.map((b) => b.build(sharedDb));
|
|
665
|
+
const batchResults = await sharedDb.batch(
|
|
666
|
+
items
|
|
667
|
+
);
|
|
668
|
+
const tables = /* @__PURE__ */ new Set();
|
|
669
|
+
for (const b of batchables) {
|
|
670
|
+
tables.add(b.tableName);
|
|
671
|
+
}
|
|
672
|
+
for (const t of tables) {
|
|
673
|
+
cacheManager?.invalidateTable(t);
|
|
674
|
+
}
|
|
675
|
+
return batchables.map((b, i) => b.withResult ? batchResults[i] : void 0);
|
|
676
|
+
}
|
|
587
677
|
const results = [];
|
|
588
678
|
for (const op of operations) {
|
|
589
679
|
results.push(await op.run(p));
|
|
@@ -636,9 +726,101 @@ function composeSequential(operations) {
|
|
|
636
726
|
}
|
|
637
727
|
};
|
|
638
728
|
}
|
|
729
|
+
function createSentinel() {
|
|
730
|
+
const target = function() {
|
|
731
|
+
};
|
|
732
|
+
return new Proxy(target, {
|
|
733
|
+
get(_t, prop) {
|
|
734
|
+
if (prop === "then") return void 0;
|
|
735
|
+
if (prop === Symbol.toPrimitive) return () => "[sentinel]";
|
|
736
|
+
if (prop === "toString") return () => "[sentinel]";
|
|
737
|
+
if (prop === "valueOf") return () => 0;
|
|
738
|
+
return createSentinel();
|
|
739
|
+
},
|
|
740
|
+
apply() {
|
|
741
|
+
return createSentinel();
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function wrapForTracking(target, perms) {
|
|
746
|
+
return new Proxy(target, {
|
|
747
|
+
get(t, prop, receiver) {
|
|
748
|
+
const orig = Reflect.get(t, prop, receiver);
|
|
749
|
+
if (prop === "permissions") return orig;
|
|
750
|
+
if (prop === "run" && typeof orig === "function") {
|
|
751
|
+
return async () => {
|
|
752
|
+
const opPerms = t.permissions;
|
|
753
|
+
if (Array.isArray(opPerms)) {
|
|
754
|
+
perms.push(...opPerms);
|
|
755
|
+
}
|
|
756
|
+
return createSentinel();
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (typeof orig === "function") {
|
|
760
|
+
return (...args) => {
|
|
761
|
+
const result = orig.apply(t, args);
|
|
762
|
+
if (result && typeof result === "object") {
|
|
763
|
+
return wrapForTracking(result, perms);
|
|
764
|
+
}
|
|
765
|
+
return result;
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
return orig;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function createTrackingDb(real, perms) {
|
|
773
|
+
return {
|
|
774
|
+
query: (table) => wrapForTracking(real.query(table), perms),
|
|
775
|
+
insert: (table) => wrapForTracking(real.insert(table), perms),
|
|
776
|
+
update: (table) => wrapForTracking(real.update(table), perms),
|
|
777
|
+
delete: (table) => wrapForTracking(real.delete(table), perms),
|
|
778
|
+
unsafe: () => createTrackingDb(real.unsafe(), perms),
|
|
779
|
+
batch: (operations) => {
|
|
780
|
+
for (const op of operations) {
|
|
781
|
+
perms.push(...op.permissions);
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
permissions: deduplicateDescriptors(operations.flatMap((op) => op.permissions)),
|
|
785
|
+
async run() {
|
|
786
|
+
return [createSentinel()];
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
cache: real.cache
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function composeSequentialCallback(db, callback) {
|
|
794
|
+
const collected = [];
|
|
795
|
+
const trackingDb = createTrackingDb(db, collected);
|
|
796
|
+
const dryRunPromise = (async () => {
|
|
797
|
+
try {
|
|
798
|
+
await callback(trackingDb);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
})();
|
|
802
|
+
const permissions = [];
|
|
803
|
+
const populated = dryRunPromise.then(() => {
|
|
804
|
+
for (const d of deduplicateDescriptors(collected)) {
|
|
805
|
+
permissions.push(d);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
return {
|
|
809
|
+
permissions,
|
|
810
|
+
async permissionsAsync() {
|
|
811
|
+
await populated;
|
|
812
|
+
return permissions;
|
|
813
|
+
},
|
|
814
|
+
async run(_params) {
|
|
815
|
+
await populated;
|
|
816
|
+
return callback(db);
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
}
|
|
639
820
|
export {
|
|
640
821
|
compose,
|
|
641
822
|
composeSequential,
|
|
823
|
+
composeSequentialCallback,
|
|
642
824
|
createDb,
|
|
643
825
|
parseCursorParams,
|
|
644
826
|
parseOffsetParams
|
package/llms.txt
CHANGED
|
@@ -23,13 +23,17 @@ import * as schema from "./schema"; // must be `import *`
|
|
|
23
23
|
|
|
24
24
|
const db = createDb({
|
|
25
25
|
d1: D1Database, // env.DB
|
|
26
|
-
schema
|
|
26
|
+
schema, // typeof import("./schema") -- no cast needed
|
|
27
27
|
grants: Grant[], // from resolveGrants(permissions, roles)
|
|
28
28
|
user: { id: string } | null, // null = anonymous
|
|
29
29
|
cache?: CacheConfig | false, // default: { backend: "cache-api" }
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`schema` is typed as `Record<string, unknown>` so you can pass `import * as schema`
|
|
34
|
+
directly even when the module also exports Drizzle `relations()`. Non-table entries
|
|
35
|
+
are ignored at runtime when looking up tables by key.
|
|
36
|
+
|
|
33
37
|
### Operation<TResult>
|
|
34
38
|
|
|
35
39
|
```typescript
|
|
@@ -60,6 +64,24 @@ db.delete(table).where(cond): DeleteReturningBuilder
|
|
|
60
64
|
|
|
61
65
|
All write builders are `Operation<void>` with an optional `.returning()` that changes return type.
|
|
62
66
|
|
|
67
|
+
#### Single-op shorthand (no compose required)
|
|
68
|
+
|
|
69
|
+
For a single mutation, call `.run()` directly on the Operation -- there's no need to wrap
|
|
70
|
+
it in `compose()` or `db.batch()`. Reserve those for multi-op workflows.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// Insert
|
|
74
|
+
const card = await db.insert(cards).values({ title: "New" }).returning().run();
|
|
75
|
+
|
|
76
|
+
// Update
|
|
77
|
+
await db.update(cards).set({ archived: true }).where(eq(cards.id, id)).run();
|
|
78
|
+
|
|
79
|
+
// Delete
|
|
80
|
+
await db.delete(cards).where(eq(cards.id, id)).run();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`.run()` arguments are optional -- only pass `params` when your query uses `sql.placeholder()`.
|
|
84
|
+
|
|
63
85
|
### compose(operations, executor): Operation<TResult>
|
|
64
86
|
|
|
65
87
|
```typescript
|
|
@@ -89,9 +111,70 @@ await op.run(); // runs operations in order, returns results array
|
|
|
89
111
|
|
|
90
112
|
Runs operations in order, returns results array. Shorthand for `compose` when there are no data dependencies between operations.
|
|
91
113
|
|
|
114
|
+
### composeSequentialCallback(db, callback): Operation<TResult>
|
|
115
|
+
|
|
116
|
+
For workflows where later operations depend on the **results** of earlier ones
|
|
117
|
+
(e.g. inserting a child row that references the inserted parent's id):
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { composeSequentialCallback } from "@cfast/db";
|
|
121
|
+
|
|
122
|
+
const op = composeSequentialCallback(db, async (tx) => {
|
|
123
|
+
const card = await tx.insert(cards).values({ title: "New" }).returning().run();
|
|
124
|
+
await tx.insert(activityLog).values({
|
|
125
|
+
cardId: (card as { id: string }).id,
|
|
126
|
+
action: "card_created",
|
|
127
|
+
}).run();
|
|
128
|
+
return card;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await op.permissionsAsync(); // [{ create, cards }, { create, activityLog }]
|
|
132
|
+
await op.run();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The callback runs **twice**: first against a tracking proxy that records each
|
|
136
|
+
operation's permission descriptors and returns sentinel values from `.run()`,
|
|
137
|
+
then against the real db. Because of the dry-run pass, the callback **must be
|
|
138
|
+
free of side effects outside of `db.*` calls** (no `fetch`, no global state
|
|
139
|
+
mutation, no `console.log` you care about).
|
|
140
|
+
|
|
141
|
+
`op.permissions` is populated asynchronously after the dry-run resolves; use
|
|
142
|
+
`op.permissionsAsync()` for a guaranteed-populated array. `op.run()` always
|
|
143
|
+
awaits the dry-run internally, so permissions are checked on every sub-op.
|
|
144
|
+
|
|
145
|
+
Use this only when you actually need data dependencies. For independent
|
|
146
|
+
operations, prefer `composeSequential([...])` (faster, fully synchronous
|
|
147
|
+
permissions).
|
|
148
|
+
|
|
92
149
|
### db.batch(operations): Operation<unknown[]>
|
|
93
150
|
|
|
94
|
-
Runs operations
|
|
151
|
+
Runs multiple operations with merged, deduplicated permissions. **Atomic** when
|
|
152
|
+
every operation was produced by `db.insert/update/delete`: the batch is sent to
|
|
153
|
+
D1's native `batch()` API, which executes the statements as a single transaction
|
|
154
|
+
and rolls everything back if any statement fails.
|
|
155
|
+
|
|
156
|
+
Use this whenever you need transactional safety -- for example, decrementing
|
|
157
|
+
stock across multiple products during checkout, or inserting an order plus its
|
|
158
|
+
line items together:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// Atomic checkout: stock decrements + order creation either all succeed or
|
|
162
|
+
// all roll back. Permissions for every sub-op are checked upfront.
|
|
163
|
+
await db.batch([
|
|
164
|
+
db.update(products).set({ stock: sql`stock - 1` }).where(eq(products.id, id1)),
|
|
165
|
+
db.update(products).set({ stock: sql`stock - 1` }).where(eq(products.id, id2)),
|
|
166
|
+
db.insert(orders).values({ userId, items: [...] }),
|
|
167
|
+
]).run();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Permission checks happen **before** any SQL is issued. If the user lacks any
|
|
171
|
+
required grant, the batch throws and zero statements run.
|
|
172
|
+
|
|
173
|
+
Operations produced by `compose()`/`composeSequential()` executors don't carry
|
|
174
|
+
the batchable hook; mixing them into `db.batch([...])` causes a fallback to
|
|
175
|
+
sequential execution (and loses the atomicity guarantee). For pure compose
|
|
176
|
+
workflows that need atomicity, build the underlying ops with `db.insert/update/
|
|
177
|
+
delete` directly and pass them straight to `db.batch([...])`.
|
|
95
178
|
|
|
96
179
|
### db.unsafe(): Db
|
|
97
180
|
|
|
@@ -177,6 +260,26 @@ export async function action({ context, request }) {
|
|
|
177
260
|
- **@cfast/auth** -- `auth.requireUser(request)` returns `{ user, grants }` which you pass directly to `createDb()`.
|
|
178
261
|
- **@cfast/actions** -- Actions define operations using `@cfast/db`. The framework extracts `.permissions` for client-side UI adaptation.
|
|
179
262
|
|
|
263
|
+
## Schema Gotchas
|
|
264
|
+
|
|
265
|
+
### Self-referential foreign keys
|
|
266
|
+
|
|
267
|
+
Drizzle cannot infer the return type of a `references()` callback that points at the same table. Without an explicit annotation, TypeScript fails with `TS7022: ... implicitly has type 'any'`. Annotate the callback with `AnySQLiteColumn`:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { sqliteTable, text, type AnySQLiteColumn } from "drizzle-orm/sqlite-core";
|
|
271
|
+
|
|
272
|
+
export const folders = sqliteTable("folders", {
|
|
273
|
+
id: text("id").primaryKey(),
|
|
274
|
+
name: text("name").notNull(),
|
|
275
|
+
parentFolderId: text("parent_folder_id").references(
|
|
276
|
+
(): AnySQLiteColumn => folders.id,
|
|
277
|
+
),
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Use `AnyPgColumn` / `AnyMySqlColumn` for other dialects. The same pattern applies to any self-reference (comment threads, org charts, category trees).
|
|
282
|
+
|
|
180
283
|
## Common Mistakes
|
|
181
284
|
|
|
182
285
|
- **Forgetting `.run()`** -- Operations are lazy. `db.query(t).findMany()` returns an Operation, not results. You must call `.run()`.
|
|
@@ -184,3 +287,9 @@ export async function action({ context, request }) {
|
|
|
184
287
|
- **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
288
|
- **Using `db.unsafe()` for admin endpoints** -- Use a role with `grant("manage", "all")` instead. Reserve `unsafe()` for genuinely user-less contexts (cron, migrations).
|
|
186
289
|
- **Expecting relational `with` to have permission filters** -- Permission WHERE clauses only apply to the root table, not joined relations.
|
|
290
|
+
- **Self-referential FK without `AnySQLiteColumn` annotation** -- TypeScript fails with TS7022. See "Schema Gotchas" above.
|
|
291
|
+
|
|
292
|
+
## See Also
|
|
293
|
+
|
|
294
|
+
- `@cfast/permissions` -- Defines the grants checked by every Operation.
|
|
295
|
+
- `@cfast/actions` -- Surfaces Operation `.permissions` to the client.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cfast/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Permission-aware Drizzle queries for Cloudflare D1",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cfast",
|
|
@@ -33,18 +33,11 @@
|
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
35
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
|
-
},
|
|
43
36
|
"peerDependencies": {
|
|
44
37
|
"drizzle-orm": ">=0.35"
|
|
45
38
|
},
|
|
46
39
|
"dependencies": {
|
|
47
|
-
"@cfast/permissions": "
|
|
40
|
+
"@cfast/permissions": "0.2.0"
|
|
48
41
|
},
|
|
49
42
|
"peerDependenciesMeta": {
|
|
50
43
|
"@cloudflare/workers-types": {
|
|
@@ -57,5 +50,12 @@
|
|
|
57
50
|
"tsup": "^8",
|
|
58
51
|
"typescript": "^5.7",
|
|
59
52
|
"vitest": "^4.1.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
56
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
57
|
+
"typecheck": "tsc --noEmit",
|
|
58
|
+
"lint": "eslint src/",
|
|
59
|
+
"test": "vitest run"
|
|
60
60
|
}
|
|
61
|
-
}
|
|
61
|
+
}
|