cloudflare-next-intl 0.8.4 → 0.8.6
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 +217 -12
- package/bin/db_codegen.mjs +58 -8
- package/bin/db_install_exec.mjs +16 -0
- package/bin/install_exec_step.mjs +39 -0
- package/dist/src/db/codegen_paths.d.ts +12 -0
- package/dist/src/db/codegen_paths.js +33 -2
- package/dist/src/db/context.js +19 -1
- package/dist/src/db/encode_param.d.ts +10 -0
- package/dist/src/db/encode_param.js +60 -0
- package/dist/src/db/helpers.d.ts +2 -2
- package/dist/src/db/helpers.js +2 -2
- package/dist/src/db/index.d.ts +9 -0
- package/dist/src/db/index.js +8 -0
- package/dist/src/db/inline_params.d.ts +18 -0
- package/dist/src/db/inline_params.js +118 -0
- package/dist/src/db/install_exec.d.ts +24 -0
- package/dist/src/db/install_exec.js +42 -0
- package/dist/src/db/parse_composite.d.ts +17 -0
- package/dist/src/db/parse_composite.js +60 -0
- package/dist/src/db/resolve_raw_sql.d.ts +24 -0
- package/dist/src/db/resolve_raw_sql.js +55 -0
- package/dist/src/db/schema.d.ts +9 -0
- package/dist/src/db/schema.js +9 -0
- package/dist/src/db/supabase_rest.d.ts +178 -0
- package/dist/src/db/supabase_rest.js +262 -0
- package/dist/src/db/supabase_transport.d.ts +8 -0
- package/dist/src/db/supabase_transport.js +41 -2
- package/dist/src/types/types.d.ts +13 -0
- package/llms.txt +4 -2
- package/package.json +278 -273
- package/supabase/cfni_exec.sql +131 -16
- package/supabase/tests/cfni_exec.sql +150 -0
package/README.md
CHANGED
|
@@ -464,12 +464,15 @@ const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
|
|
|
464
464
|
|
|
465
465
|
Supabase mode requires one function in your database, shipped at
|
|
466
466
|
`node_modules/cloudflare-next-intl/supabase/cfni_exec.sql`. Run it once (via
|
|
467
|
-
`supabase db push`, a migration, or the SQL editor)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
467
|
+
`supabase db push`, a migration, or the SQL editor) — the file starts with a
|
|
468
|
+
`drop function if exists` so re-running it to upgrade is always safe. It is
|
|
469
|
+
`security invoker`, so statements execute with the caller's own privileges and
|
|
470
|
+
RLS applies exactly as it does over the REST API; **never** change this to
|
|
471
|
+
`security definer` — that would let any caller (anon included) bypass RLS
|
|
472
|
+
through the function, regardless of the role PostgREST resolved them as.
|
|
473
|
+
`@supabase/supabase-js` ships as a dependency of this package and is loaded
|
|
474
|
+
through dynamic `import()` inside the `db` exports, same as `pg`/`drizzle-orm`
|
|
475
|
+
— an app that never calls a `db` export never bundles any of them.
|
|
473
476
|
|
|
474
477
|
`db.supabase` fields (all optional):
|
|
475
478
|
|
|
@@ -479,6 +482,10 @@ bundles any of them.
|
|
|
479
482
|
Defaults to `NEXT_PUBLIC_SUPABASE_ANON_KEY`. Never put a service-role key
|
|
480
483
|
here.
|
|
481
484
|
- `execFunction` — name of the exec function. Defaults to `'cfni_exec'`.
|
|
485
|
+
- `rawSql` — whether `withPublicDb`/`withUserDb` may run arbitrary SQL
|
|
486
|
+
through `cfni_exec`. Defaults to `true`. Set `false` if you don't want to
|
|
487
|
+
(or can't) install `cfni_exec` — see
|
|
488
|
+
[Supabase mode without `cfni_exec`](#supabase-mode-without-cfni_exec) below.
|
|
482
489
|
|
|
483
490
|
Any of `connectionString`, `supabase.url`, and `supabase.anonKey` may be a
|
|
484
491
|
function instead of a string, resolved (and awaited) at use time rather than
|
|
@@ -516,7 +523,94 @@ user's Firebase ID token is used automatically.
|
|
|
516
523
|
role that can execute it can run arbitrary SQL *within that role's own
|
|
517
524
|
privileges* — a broader surface than PostgREST's normal verbs, though still
|
|
518
525
|
bounded by RLS and your grants. If your app only uses `withUserDb`, drop the
|
|
519
|
-
anon grant: `revoke execute on function public.cfni_exec(text
|
|
526
|
+
anon grant: `revoke execute on function public.cfni_exec(text) from anon;`
|
|
527
|
+
|
|
528
|
+
**What `cfni_exec` actually supports:** plain `SELECT` (including joins with
|
|
529
|
+
duplicate column names, CTEs, window functions), `INSERT`/`UPDATE`/`DELETE`
|
|
530
|
+
with or without `RETURNING` (including `ON CONFLICT ... DO UPDATE` via
|
|
531
|
+
`excluded`/`onConflictSet` from `cloudflare-next-intl/dbHelpers`), and
|
|
532
|
+
writable CTEs (`with x as (update ... returning ...) select ... from x`).
|
|
533
|
+
Every value round-trips through Postgres' own text representation, not JSON,
|
|
534
|
+
so arrays/`numeric`/timestamps/`bytea` decode the same way they do in
|
|
535
|
+
connection-string mode. Not supported: multi-statement transactions (each
|
|
536
|
+
call is its own PostgREST round-trip — see above).
|
|
537
|
+
|
|
538
|
+
#### Supabase mode without `cfni_exec`
|
|
539
|
+
|
|
540
|
+
If you don't want to install `cfni_exec` at all, set `db.supabase.rawSql:
|
|
541
|
+
false`. `withPublicDb`/`withUserDb` then throw immediately in Supabase mode
|
|
542
|
+
instead of failing later against PostgREST, and you reach Postgres instead
|
|
543
|
+
through a set of helpers that call `@supabase/supabase-js`'s `.from()`/`.rpc()`
|
|
544
|
+
API directly — the same REST surface the Supabase client itself uses, no raw
|
|
545
|
+
SQL involved:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
import { supabaseSelect, supabaseInsert, supabaseUpdate, supabaseDelete, supabaseUpsert, supabaseRpc } from "cloudflare-next-intl/db";
|
|
549
|
+
|
|
550
|
+
const { rows } = await supabaseSelect("bonds", {
|
|
551
|
+
where: { active: true, yield: ["gte", 5] },
|
|
552
|
+
orderBy: { column: "yield", ascending: false },
|
|
553
|
+
limit: 10,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await supabaseInsert("bonds", { name: "10Y", yield: 4.2 });
|
|
557
|
+
await supabaseUpsert("bonds", { id: 1, yield: 4.5 }, { onConflict: "id" });
|
|
558
|
+
await supabaseUpdate("bonds", { yield: 4.3 }, { where: { id: 1 } });
|
|
559
|
+
await supabaseDelete("bonds", { where: { id: 1 } });
|
|
560
|
+
|
|
561
|
+
// Anything a plain select/insert/update/delete/upsert can't express (joins,
|
|
562
|
+
// aggregates, custom logic) — write a Postgres function, `grant execute` to
|
|
563
|
+
// `anon`/`authenticated`, and call it by name. No cfni_exec needed for this
|
|
564
|
+
// either, since PostgREST always supports calling a named function you own.
|
|
565
|
+
const total = await supabaseRpc("total_yield", { bond_ids: [1, 2, 3] });
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
Each has a `*AsUser` counterpart (`supabaseSelectAsUser`,
|
|
569
|
+
`supabaseInsertAsUser`, `supabaseUpsertAsUser`, `supabaseUpdateAsUser`,
|
|
570
|
+
`supabaseDeleteAsUser`, `supabaseRpcAsUser`) that authenticates as the
|
|
571
|
+
signed-in user instead of anon — same token resolution as `withUserDb`
|
|
572
|
+
(`db.getAccessToken`, or the signed-in Firebase user's ID token), so RLS
|
|
573
|
+
applies the same way.
|
|
574
|
+
|
|
575
|
+
`supabaseSelect`/`supabaseSelectAsUser` accept:
|
|
576
|
+
|
|
577
|
+
- `columns` — PostgREST column-list syntax. Defaults to `'*'`. Supports an
|
|
578
|
+
embedded resource for a foreign-key join in one round-trip, e.g.
|
|
579
|
+
`'*, author(name)'`.
|
|
580
|
+
- `where` — filters, ANDed together. A bare value (`{ id: 5 }`) is shorthand
|
|
581
|
+
for `eq`; use `{ column: [operator, value] }` for any other operator —
|
|
582
|
+
`neq`, `gt`, `gte`, `lt`, `lte`, `like`, `likeAllOf`, `likeAnyOf`, `ilike`,
|
|
583
|
+
`ilikeAllOf`, `ilikeAnyOf`, `regexMatch`, `regexIMatch`, `is`,
|
|
584
|
+
`isDistinct`, `in`, `contains`, `containedBy`, `overlaps`, `rangeGt`,
|
|
585
|
+
`rangeGte`, `rangeLt`, `rangeLte`, or `rangeAdjacent` — or
|
|
586
|
+
`{ column: ['not', operator, value] }` to negate any of them. This is the
|
|
587
|
+
complete operator set `@supabase/supabase-js`'s `.from()` builder exposes
|
|
588
|
+
(it *is* a `postgrest-js` builder — there's no separate, larger "Supabase
|
|
589
|
+
client" query surface on top of it).
|
|
590
|
+
- `match` — shorthand for several `eq` filters at once (`{ status: 'active',
|
|
591
|
+
archived: false }`). ANDed with `where`.
|
|
592
|
+
- `or` — a raw PostgREST `or()` filter string (e.g.
|
|
593
|
+
`'age.gt.18,status.eq.active'`) for filters spanning multiple columns in
|
|
594
|
+
one clause; ANDed with `where`/`match`.
|
|
595
|
+
- `textSearch` — `{ column, query, type?, config? }` for full-text search via
|
|
596
|
+
PostgREST's `@@` operators.
|
|
597
|
+
- `orderBy` — one `{ column, ascending?, nullsFirst? }`, or an array for
|
|
598
|
+
multiple `order by` clauses.
|
|
599
|
+
- `limit` / `range` — row limit, or a `[from, to]` PostgREST pagination range.
|
|
600
|
+
- `single` / `maybeSingle` — resolve to one row (erroring if there isn't
|
|
601
|
+
exactly one), or one row or `null` (erroring if there's more than one).
|
|
602
|
+
- `count` — also return the total matching row count (`'exact'`, `'planned'`,
|
|
603
|
+
or `'estimated'`).
|
|
604
|
+
|
|
605
|
+
`supabaseUpdate`/`supabaseDelete` (and their `*AsUser` forms) accept `where`/
|
|
606
|
+
`match`/`or` the same way, and require at least one of them — an unfiltered
|
|
607
|
+
update/delete would otherwise touch every row.
|
|
608
|
+
|
|
609
|
+
This mode trades capability for not needing `cfni_exec`: everything the
|
|
610
|
+
Supabase JS client itself can express against a single table (including an
|
|
611
|
+
embedded-resource join) works; multi-table joins beyond that, aggregates,
|
|
612
|
+
window functions, and raw SQL of any kind do not — those need `cfni_exec`, or
|
|
613
|
+
a Postgres function called via `supabaseRpc`.
|
|
520
614
|
|
|
521
615
|
Two query wrappers, both from `cloudflare-next-intl/db`. Choose by who is
|
|
522
616
|
allowed to see the rows:
|
|
@@ -551,12 +645,94 @@ helpers (`excluded`, `onConflictSet`, `ago`, `currentDate`, `windowCount`,
|
|
|
551
645
|
upsert/window/lateral-join queries without dropping to raw SQL, plus a
|
|
552
646
|
re-export of `drizzle-orm`'s common query-building primitives (`eq`, `and`,
|
|
553
647
|
`or`, `asc`, `desc`, `gte`, `gt`, `lte`, `lt`, `isNull`, `isNotNull`, `count`,
|
|
554
|
-
`sum`, `max`, `min`, `sql`
|
|
648
|
+
`sum`, `max`, `min`, `sql`, `inArray`, `notInArray`, `ne`, `like`, `ilike`,
|
|
649
|
+
`between`, `not`, `exists`) — so code that only builds queries against the
|
|
555
650
|
`DrizzleDb` handle from `withPublicDb`/`withUserDb` doesn't need its own
|
|
556
|
-
`drizzle-orm` import for these.
|
|
557
|
-
|
|
558
|
-
`
|
|
559
|
-
|
|
651
|
+
`drizzle-orm` import for these.
|
|
652
|
+
|
|
653
|
+
For schema definitions, `cloudflare-next-intl/dbSchema` re-exports the
|
|
654
|
+
`drizzle-orm/pg-core` table and column builders (`pgTable`, `varchar`,
|
|
655
|
+
`integer`, `index`, `pgEnum`, …) alongside the `sql` tag, so generated schema
|
|
656
|
+
files can import from this package too:
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
import { pgTable, varchar, integer } from 'cloudflare-next-intl/dbSchema';
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Relations and any other advanced Drizzle surface still come from `drizzle-orm`
|
|
663
|
+
directly.
|
|
664
|
+
|
|
665
|
+
#### Schema codegen (`cfni-db-codegen`)
|
|
666
|
+
|
|
667
|
+
The package ships a `cfni-db-codegen` binary that regenerates Drizzle models by
|
|
668
|
+
introspecting a live Postgres with `drizzle-kit pull`, patches drizzle-kit's
|
|
669
|
+
bare function-call defaults into raw-SQL-wrapped ones, and writes a
|
|
670
|
+
`manifest.json` next to the schema so `--check` can fail CI when the DDL
|
|
671
|
+
changed without regenerating.
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
npx cfni-db-codegen
|
|
675
|
+
npx cfni-db-codegen --check
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
| Flag | Env | Default |
|
|
679
|
+
| --- | --- | --- |
|
|
680
|
+
| `--ddl-dir=` | `CFNI_DB_DDL_DIR` | `supabase/data-base` |
|
|
681
|
+
| `--out-dir=` | `CFNI_DB_OUT_DIR` | `src/shared/db/generated` |
|
|
682
|
+
| `--out-file=` | `CFNI_DB_OUT_FILE` | `schema.ts` |
|
|
683
|
+
| `--db-url=` | `CODEGEN_DATABASE_URL` | `postgresql://postgres:postgres@127.0.0.1:54322/postgres` |
|
|
684
|
+
| `--drizzle-config=` | `CFNI_DB_DRIZZLE_CONFIG` | none |
|
|
685
|
+
| `--rpc-dir=` | `CFNI_DB_RPC_DIR` | sibling of `--ddl-dir`, e.g. `supabase/rpc` |
|
|
686
|
+
| `--tests-dir=` | `CFNI_DB_TESTS_DIR` | sibling of `--ddl-dir`, e.g. `supabase/tests` |
|
|
687
|
+
| `--force` | `CFNI_DB_FORCE_EXEC=true` | off |
|
|
688
|
+
| `--skip-exec` | `CFNI_DB_SKIP_EXEC=true` | off |
|
|
689
|
+
| `--check` | — | off |
|
|
690
|
+
|
|
691
|
+
`--out-dir` may be repeated, or given a comma-separated list, to generate the
|
|
692
|
+
same schema into several projects in one run (`CFNI_DB_OUT_DIR` accepts a
|
|
693
|
+
comma-separated list too). The database is introspected once and the identical
|
|
694
|
+
schema plus manifest is written to every target; `--check` verifies all of
|
|
695
|
+
them and fails naming the first one that is stale.
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
npx cfni-db-codegen --out-dir=src/shared/db/generated --out-dir=../other-app/src/db/generated
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
##### Keeping `cfni_exec.sql` in sync (`--rpc-dir`/`--tests-dir`/`--force`/`--skip-exec`)
|
|
702
|
+
|
|
703
|
+
After a successful (non-`--check`) run, `cfni-db-codegen` also copies
|
|
704
|
+
`supabase/cfni_exec.sql` and its pgTAP test file (see
|
|
705
|
+
[Testing `cfni_exec.sql` itself](#testing-cfni_execsql-itself) below) into
|
|
706
|
+
your project — `--rpc-dir`/`--tests-dir` (defaulting to sibling folders of
|
|
707
|
+
`--ddl-dir`, so `rpc`/`tests` next to `data-base`). This keeps a project that
|
|
708
|
+
enables Supabase mode's raw-SQL path always holding the current version of the
|
|
709
|
+
function, without a manual copy-paste step.
|
|
710
|
+
|
|
711
|
+
This step is gated on `db.supabase.rawSql` (see
|
|
712
|
+
[Supabase mode without `cfni_exec`](#supabase-mode-without-cfni_exec)):
|
|
713
|
+
codegen reads your `next.config.*`'s `@intl-config` alias, opens the intl
|
|
714
|
+
config file it points at, and looks for a literal `rawSql: true`/`rawSql:
|
|
715
|
+
false`. If it's explicitly `false`, the copy is skipped entirely — no
|
|
716
|
+
`supabase/rpc`/`supabase/tests` folders are created. If it can't be
|
|
717
|
+
determined (no `next.config.*` found, no alias, or `rawSql` isn't a plain
|
|
718
|
+
`true`/`false` literal in the source), a warning is printed and codegen
|
|
719
|
+
assumes `true`, matching `withPublicDb`/`withUserDb`'s own default.
|
|
720
|
+
|
|
721
|
+
An existing target file that already matches is left untouched; one that
|
|
722
|
+
exists with **different** content (a customization, or a stale version) is
|
|
723
|
+
skipped with a warning rather than silently overwritten — pass `--force` (or
|
|
724
|
+
set `CFNI_DB_FORCE_EXEC=true`) to overwrite it anyway. Pass `--skip-exec` (or
|
|
725
|
+
set `CFNI_DB_SKIP_EXEC=true`) to turn this whole step off, independent of
|
|
726
|
+
`rawSql`.
|
|
727
|
+
|
|
728
|
+
To run only this step — no `drizzle-kit pull`, no live Postgres needed — use
|
|
729
|
+
the standalone `cfni-db-install-exec` binary instead, which accepts the same
|
|
730
|
+
`--rpc-dir`/`--tests-dir`/`--force` flags:
|
|
731
|
+
|
|
732
|
+
```bash
|
|
733
|
+
npx cfni-db-install-exec
|
|
734
|
+
npx cfni-db-install-exec --force
|
|
735
|
+
```
|
|
560
736
|
|
|
561
737
|
#### Testing code that calls `withPublicDb`/`withUserDb`
|
|
562
738
|
|
|
@@ -581,6 +757,35 @@ just "select was called" but "the second select's `.where(...)` argument was
|
|
|
581
757
|
X". Handles `db.$with(name).as(builder)` / `db.with(...).select(...)`
|
|
582
758
|
CTE-style queries the same way the real Drizzle client does.
|
|
583
759
|
|
|
760
|
+
#### Testing `cfni_exec.sql` itself
|
|
761
|
+
|
|
762
|
+
Because `cfni_exec.sql` runs real SQL — statement classification (SELECT vs.
|
|
763
|
+
DML, writable CTEs), literal-encoding of parameters, and RLS behavior — it's
|
|
764
|
+
tested two ways in this package's own repo, and both are shipped so you can
|
|
765
|
+
reuse them against your own database rather than trusting the function
|
|
766
|
+
untested:
|
|
767
|
+
|
|
768
|
+
- `supabase/tests/cfni_exec.sql` — a [pgTAP](https://pgtap.org/) suite,
|
|
769
|
+
runnable with `supabase test db` (or `pg_prove`) once both this file and
|
|
770
|
+
`cfni_exec.sql` are installed in a database. It checks the SQL function
|
|
771
|
+
directly: every statement shape `cfni_exec` classifies (plain `SELECT`,
|
|
772
|
+
`INSERT`/`UPDATE`/`DELETE` with and without `RETURNING`, writable CTEs,
|
|
773
|
+
`ON CONFLICT DO UPDATE`), value fidelity (arrays/booleans/`numeric`/`NULL`
|
|
774
|
+
round-tripping through pg's own text form, not JSON), and that
|
|
775
|
+
`SECURITY INVOKER` really does make RLS apply per-role (`anon` vs.
|
|
776
|
+
`authenticated` see different rows under the same policy). `cfni-db-codegen`
|
|
777
|
+
and `cfni-db-install-exec` copy this file into your project the same way
|
|
778
|
+
they copy `cfni_exec.sql` itself, so it's there to run against your own
|
|
779
|
+
schema whenever you want the same confidence.
|
|
780
|
+
- `src/db/cfni_exec.integration.test.ts` (in this package's source, not
|
|
781
|
+
something copied into your project) — a Vitest suite that drives the exact
|
|
782
|
+
same scenarios through the real TypeScript transport path:
|
|
783
|
+
`inlineParams` → `cfni_exec` → `parseComposite`, over an actual Postgres
|
|
784
|
+
connection. It's skipped automatically unless `CFNI_TEST_DATABASE_URL` is
|
|
785
|
+
set (e.g. to a throwaway `docker run -d -e POSTGRES_PASSWORD=postgres -p
|
|
786
|
+
55432:5432 postgres:15`), so it never runs — and never needs a database —
|
|
787
|
+
during a normal `npm test`.
|
|
788
|
+
|
|
584
789
|
## License
|
|
585
790
|
|
|
586
791
|
MIT
|
package/bin/db_codegen.mjs
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Regenerates Drizzle models by introspecting a live Postgres with drizzle-kit.
|
|
3
3
|
// Usage: cfni-db-codegen [--check] [--ddl-dir=…] [--out-dir=…] [--out-file=…] [--db-url=…] [--drizzle-config=…]
|
|
4
|
+
// [--rpc-dir=…] [--tests-dir=…] [--force] [--skip-exec]
|
|
5
|
+
//
|
|
6
|
+
// --out-dir may be repeated, or given a comma-separated list, to generate the
|
|
7
|
+
// same schema into several projects at once (CFNI_DB_OUT_DIR accepts a
|
|
8
|
+
// comma-separated list too).
|
|
4
9
|
//
|
|
5
10
|
// Needs a reachable Postgres to introspect — any Postgres, not specifically
|
|
6
11
|
// a Docker one. Set CODEGEN_DATABASE_URL to point at whichever you have:
|
|
@@ -9,12 +14,23 @@
|
|
|
9
14
|
// set, it tries the local Supabase default (127.0.0.1:54322).
|
|
10
15
|
// CODEGEN_CONNECT_TIMEOUT_MS overrides the 5s default reachability-check
|
|
11
16
|
// timeout — raise it for a slow/cold-starting remote or serverless target.
|
|
17
|
+
//
|
|
18
|
+
// After a successful (non-`--check`) run, also installs `cfni_exec.sql` (and
|
|
19
|
+
// its pgTAP test file) into `--rpc-dir`/`--tests-dir` (default siblings of
|
|
20
|
+
// `--ddl-dir`, e.g. `supabase/rpc`/`supabase/tests`) — but only when the
|
|
21
|
+
// project's `db.supabase.rawSql` isn't explicitly `false` (read from
|
|
22
|
+
// `next.config.*`'s `@intl-config` alias; a warning is printed if it can't
|
|
23
|
+
// be determined). An existing, differing file is left alone unless
|
|
24
|
+
// `--force`/CFNI_DB_FORCE_EXEC=true is set. Pass `--skip-exec`/
|
|
25
|
+
// CFNI_DB_SKIP_EXEC=true to turn this step off entirely, or use the
|
|
26
|
+
// standalone `cfni-db-install-exec` command to run only this step.
|
|
12
27
|
import { createHash } from 'node:crypto';
|
|
13
28
|
import { execFileSync } from 'node:child_process';
|
|
14
29
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
30
|
import { join, relative } from 'node:path';
|
|
16
31
|
import { Client } from 'pg';
|
|
17
32
|
import resolveCodegenPaths from '../dist/src/db/codegen_paths.js';
|
|
33
|
+
import { runInstallExecStep } from './install_exec_step.mjs';
|
|
18
34
|
|
|
19
35
|
const paths = resolveCodegenPaths(process.argv.slice(2), process.env, process.cwd());
|
|
20
36
|
|
|
@@ -55,9 +71,10 @@ function ddlHash() {
|
|
|
55
71
|
const hash = ddlHash();
|
|
56
72
|
|
|
57
73
|
if (paths.check) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
for (const target of paths.targets) {
|
|
75
|
+
const previous = existsSync(target.manifest) ? JSON.parse(readFileSync(target.manifest, "utf8")).ddlHash : null;
|
|
76
|
+
if (previous === hash) continue;
|
|
77
|
+
console.error(`❌ ${relative(process.cwd(), paths.ddlDir)} changed without regenerating models in ${relative(process.cwd(), target.outDir)}. Run: npm run db:codegen`);
|
|
61
78
|
process.exit(1);
|
|
62
79
|
}
|
|
63
80
|
console.log(`✅ Drizzle models are in sync with ${relative(process.cwd(), paths.ddlDir)}`);
|
|
@@ -69,7 +86,6 @@ await assertReachable(paths.dbUrl);
|
|
|
69
86
|
rmSync(paths.pullDir, { recursive: true, force: true });
|
|
70
87
|
execFileSync('npx', ['drizzle-kit', 'pull', ...(paths.drizzleConfig ? [`--config=${paths.drizzleConfig}`] : [])], { stdio: 'inherit' });
|
|
71
88
|
|
|
72
|
-
mkdirSync(paths.outDir, { recursive: true });
|
|
73
89
|
const pulled = join(paths.pullDir, "schema.ts");
|
|
74
90
|
if (!existsSync(pulled)) {
|
|
75
91
|
console.error(`❌ drizzle-kit pull produced no schema at ${pulled}`);
|
|
@@ -102,9 +118,43 @@ function patchBareFunctionCallDefaults(source) {
|
|
|
102
118
|
);
|
|
103
119
|
}
|
|
104
120
|
|
|
121
|
+
// drizzle-kit emits imports from `drizzle-orm/pg-core` and `drizzle-orm`.
|
|
122
|
+
// Point them at this package's re-exports so consuming projects don't need a
|
|
123
|
+
// direct `drizzle-orm` dependency just to load their generated schema.
|
|
124
|
+
function retargetDrizzleImports(source) {
|
|
125
|
+
const coreMatch = source.match(
|
|
126
|
+
/^import \{([^}]*)\} from "drizzle-orm\/pg-core";?$/m,
|
|
127
|
+
);
|
|
128
|
+
const rootMatch = source.match(/^import \{([^}]*)\} from "drizzle-orm";?$/m);
|
|
129
|
+
if (!coreMatch && !rootMatch) return source;
|
|
130
|
+
|
|
131
|
+
const names = [...(coreMatch ? [coreMatch[1]] : []), ...(rootMatch ? [rootMatch[1]] : [])]
|
|
132
|
+
.flatMap((group) => group.split(","))
|
|
133
|
+
.map((name) => name.trim())
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
const merged = `import { ${[...new Set(names)].join(", ")} } from "cloudflare-next-intl/dbSchema"`;
|
|
136
|
+
|
|
137
|
+
let seen = false;
|
|
138
|
+
return source.replace(
|
|
139
|
+
/^import \{[^}]*\} from "drizzle-orm(?:\/pg-core)?";?$/gm,
|
|
140
|
+
() => {
|
|
141
|
+
if (seen) return "";
|
|
142
|
+
seen = true;
|
|
143
|
+
return merged;
|
|
144
|
+
},
|
|
145
|
+
).replace(/\n{3,}/g, "\n\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
105
148
|
const banner = `// GENERATED by cfni-db-codegen from ${relative(process.cwd(), paths.ddlDir)} — do not edit.\n`;
|
|
106
|
-
const pulledSource =
|
|
107
|
-
|
|
149
|
+
const pulledSource = retargetDrizzleImports(
|
|
150
|
+
patchBareFunctionCallDefaults(readFileSync(pulled, "utf8")),
|
|
151
|
+
);
|
|
108
152
|
rmSync(paths.pullDir, { recursive: true, force: true });
|
|
109
|
-
|
|
110
|
-
|
|
153
|
+
for (const target of paths.targets) {
|
|
154
|
+
mkdirSync(target.outDir, { recursive: true });
|
|
155
|
+
writeFileSync(target.outFile, banner + pulledSource);
|
|
156
|
+
writeFileSync(target.manifest, `${JSON.stringify({ ddlHash: hash }, null, 2)}\n`);
|
|
157
|
+
console.log(`✅ Generated ${relative(process.cwd(), target.outFile)}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
runInstallExecStep(paths, process.cwd());
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Installs cfni_exec.sql (and its pgTAP test file) into the consuming
|
|
3
|
+
// project — the same step cfni-db-codegen runs after a successful
|
|
4
|
+
// generation, exposed standalone for when you only want this and nothing
|
|
5
|
+
// else (no drizzle-kit pull, no live Postgres needed).
|
|
6
|
+
//
|
|
7
|
+
// Usage: cfni-db-install-exec [--rpc-dir=…] [--tests-dir=…] [--force]
|
|
8
|
+
//
|
|
9
|
+
// Gated on the project's `db.supabase.rawSql` (read from `next.config.*`'s
|
|
10
|
+
// `@intl-config` alias) the same way cfni-db-codegen's step is — pass
|
|
11
|
+
// `--force`/CFNI_DB_FORCE_EXEC=true to overwrite an existing, differing file.
|
|
12
|
+
import resolveCodegenPaths from '../dist/src/db/codegen_paths.js';
|
|
13
|
+
import { runInstallExecStep } from './install_exec_step.mjs';
|
|
14
|
+
|
|
15
|
+
const paths = resolveCodegenPaths(process.argv.slice(2), process.env, process.cwd());
|
|
16
|
+
runInstallExecStep(paths, process.cwd());
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Shared by cfni-db-codegen and cfni-db-install-exec: copies cfni_exec.sql
|
|
2
|
+
// (and its pgTAP test file) from this package's own `supabase/` folder into
|
|
3
|
+
// the consuming project, gated on the project's `db.supabase.rawSql` setting.
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import resolveRawSql from '../dist/src/db/resolve_raw_sql.js';
|
|
6
|
+
import { installExecFile } from '../dist/src/db/install_exec.js';
|
|
7
|
+
|
|
8
|
+
const PACKAGE_ROOT = new URL('..', import.meta.url).pathname;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('../dist/src/db/codegen_paths.js').CodegenPaths} paths
|
|
12
|
+
* @param {string} cwd
|
|
13
|
+
*/
|
|
14
|
+
export function runInstallExecStep(paths, cwd) {
|
|
15
|
+
if (paths.skipExec) {
|
|
16
|
+
console.log('ℹ️ Skipping cfni_exec install — --skip-exec passed.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const rawSql = resolveRawSql(cwd);
|
|
21
|
+
if (rawSql.status === 'false') {
|
|
22
|
+
console.log(`ℹ️ Skipping cfni_exec install — db.supabase.rawSql is false (${rawSql.reason}).`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (rawSql.status === 'unknown') {
|
|
26
|
+
console.warn(`⚠️ Could not determine db.supabase.rawSql (${rawSql.reason}) — assuming true and installing cfni_exec. Pass --skip-exec, or set db.supabase.rawSql: false, to turn this off.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const files = [
|
|
30
|
+
{ sourcePath: join(PACKAGE_ROOT, 'supabase/cfni_exec.sql'), targetPath: paths.rpcFile },
|
|
31
|
+
{ sourcePath: join(PACKAGE_ROOT, 'supabase/tests/cfni_exec.sql'), targetPath: paths.testsFile },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
const result = installExecFile(file, paths.force);
|
|
36
|
+
const icon = { created: '✅', updated: '✅', unchanged: 'ℹ️', 'skipped-differs': '⚠️', 'skipped-missing-source': '⚠️' }[result.action];
|
|
37
|
+
console.log(`${icon} ${result.targetPath}: ${result.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
export interface CodegenTarget {
|
|
2
|
+
outDir: string;
|
|
3
|
+
outFile: string;
|
|
4
|
+
manifest: string;
|
|
5
|
+
}
|
|
1
6
|
export interface CodegenPaths {
|
|
2
7
|
ddlDir: string;
|
|
8
|
+
targets: CodegenTarget[];
|
|
3
9
|
outDir: string;
|
|
4
10
|
outFile: string;
|
|
5
11
|
pullDir: string;
|
|
@@ -8,6 +14,12 @@ export interface CodegenPaths {
|
|
|
8
14
|
check: boolean;
|
|
9
15
|
timeoutMs: number;
|
|
10
16
|
drizzleConfig: string | null;
|
|
17
|
+
rpcDir: string;
|
|
18
|
+
rpcFile: string;
|
|
19
|
+
testsDir: string;
|
|
20
|
+
testsFile: string;
|
|
21
|
+
force: boolean;
|
|
22
|
+
skipExec: boolean;
|
|
11
23
|
}
|
|
12
24
|
/** Resolves every codegen path from flags, then env, then the documented defaults. */
|
|
13
25
|
export default function resolveCodegenPaths(argv: readonly string[], env: Record<string, string | undefined>, cwd: string): CodegenPaths;
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
1
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
2
2
|
const DEFAULT_DDL_DIR = 'supabase/data-base';
|
|
3
3
|
const DEFAULT_OUT_DIR = 'src/shared/db/generated';
|
|
4
4
|
const DEFAULT_OUT_FILE = 'schema.ts';
|
|
5
5
|
const DEFAULT_DB_URL = 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
7
|
+
const DEFAULT_RPC_FILE_NAME = 'cfni_exec.sql';
|
|
8
|
+
const DEFAULT_TESTS_FILE_NAME = 'cfni_exec.sql';
|
|
9
|
+
function flags(argv, name) {
|
|
10
|
+
const prefix = `--${name}=`;
|
|
11
|
+
return argv.filter((arg) => arg.startsWith(prefix)).map((arg) => arg.slice(prefix.length));
|
|
12
|
+
}
|
|
13
|
+
function list(value) {
|
|
14
|
+
return value.split(',').map((part) => part.trim()).filter(Boolean);
|
|
15
|
+
}
|
|
7
16
|
function flag(argv, name) {
|
|
8
17
|
const prefix = `--${name}=`;
|
|
9
18
|
const hit = argv.find((arg) => arg.startsWith(prefix));
|
|
@@ -15,11 +24,27 @@ function abs(cwd, value) {
|
|
|
15
24
|
/** Resolves every codegen path from flags, then env, then the documented defaults. */
|
|
16
25
|
export default function resolveCodegenPaths(argv, env, cwd) {
|
|
17
26
|
const ddlDir = abs(cwd, flag(argv, 'ddl-dir') ?? env.CFNI_DB_DDL_DIR ?? DEFAULT_DDL_DIR);
|
|
18
|
-
const
|
|
27
|
+
const outDirArgs = flags(argv, 'out-dir').flatMap(list);
|
|
28
|
+
const outDirs = (outDirArgs.length > 0
|
|
29
|
+
? outDirArgs
|
|
30
|
+
: list(env.CFNI_DB_OUT_DIR ?? '')).map((dir) => abs(cwd, dir));
|
|
31
|
+
if (outDirs.length === 0)
|
|
32
|
+
outDirs.push(abs(cwd, DEFAULT_OUT_DIR));
|
|
33
|
+
const outDir = outDirs[0];
|
|
19
34
|
const outFileName = flag(argv, 'out-file') ?? env.CFNI_DB_OUT_FILE ?? DEFAULT_OUT_FILE;
|
|
20
35
|
const drizzleConfig = flag(argv, 'drizzle-config') ?? env.CFNI_DB_DRIZZLE_CONFIG ?? null;
|
|
36
|
+
// Sibling of ddlDir (default `supabase/data-base` → `supabase`), matching
|
|
37
|
+
// where `cfni_exec.sql` ships in this package's own `supabase/` folder.
|
|
38
|
+
const supabaseRoot = dirname(ddlDir);
|
|
39
|
+
const rpcDir = abs(cwd, flag(argv, 'rpc-dir') ?? env.CFNI_DB_RPC_DIR ?? join(supabaseRoot, 'rpc'));
|
|
40
|
+
const testsDir = abs(cwd, flag(argv, 'tests-dir') ?? env.CFNI_DB_TESTS_DIR ?? join(supabaseRoot, 'tests'));
|
|
21
41
|
return {
|
|
22
42
|
ddlDir,
|
|
43
|
+
targets: outDirs.map((dir) => ({
|
|
44
|
+
outDir: dir,
|
|
45
|
+
outFile: join(dir, outFileName),
|
|
46
|
+
manifest: join(dir, 'manifest.json'),
|
|
47
|
+
})),
|
|
23
48
|
outDir,
|
|
24
49
|
outFile: join(outDir, outFileName),
|
|
25
50
|
pullDir: resolve(outDir, '..', '.drizzle-pull'),
|
|
@@ -28,5 +53,11 @@ export default function resolveCodegenPaths(argv, env, cwd) {
|
|
|
28
53
|
check: argv.includes('--check'),
|
|
29
54
|
timeoutMs: Number(env.CODEGEN_CONNECT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS,
|
|
30
55
|
drizzleConfig: drizzleConfig === null ? null : abs(cwd, drizzleConfig),
|
|
56
|
+
rpcDir,
|
|
57
|
+
rpcFile: join(rpcDir, DEFAULT_RPC_FILE_NAME),
|
|
58
|
+
testsDir,
|
|
59
|
+
testsFile: join(testsDir, DEFAULT_TESTS_FILE_NAME),
|
|
60
|
+
force: argv.includes('--force') || env.CFNI_DB_FORCE_EXEC === 'true',
|
|
61
|
+
skipExec: argv.includes('--skip-exec') || env.CFNI_DB_SKIP_EXEC === 'true',
|
|
31
62
|
};
|
|
32
63
|
}
|
package/dist/src/db/context.js
CHANGED
|
@@ -27,13 +27,31 @@ async function resolveUserId(uid) {
|
|
|
27
27
|
throw new Error('db: withUserDb could not resolve a user id. Pass one explicitly, set ' +
|
|
28
28
|
'`db.getUserId`, or configure `firebaseAuth` so the signed-in Firebase uid is used.');
|
|
29
29
|
}
|
|
30
|
+
function requireRawSql(supabase) {
|
|
31
|
+
if (supabase.rawSql === false) {
|
|
32
|
+
throw new Error('db: withPublicDb/withUserDb need `cfni_exec` to run SQL in Supabase mode, but ' +
|
|
33
|
+
'`db.supabase.rawSql` is set to `false`. Use `supabaseSelect`/`supabaseInsert`/' +
|
|
34
|
+
'`supabaseUpdate`/`supabaseDelete` instead, which call the Supabase REST API directly.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
30
37
|
/**
|
|
31
38
|
* Builds a Drizzle handle backed by PostgREST. `bearerToken` decides the role
|
|
32
39
|
* Postgres sees: the anon key for public access, a user JWT for `withUserDb`.
|
|
33
40
|
*/
|
|
34
41
|
async function supabaseDb(supabase, bearerToken) {
|
|
42
|
+
requireRawSql(supabase);
|
|
35
43
|
const { drizzle } = await import('drizzle-orm/pg-proxy');
|
|
36
|
-
|
|
44
|
+
const db = drizzle(createSupabaseTransport(supabase, bearerToken));
|
|
45
|
+
return Object.assign(db, {
|
|
46
|
+
// pg-proxy has no session to open a real transaction over — every
|
|
47
|
+
// statement is its own PostgREST round-trip — so failing loudly here
|
|
48
|
+
// beats silently running the callback non-atomically.
|
|
49
|
+
transaction() {
|
|
50
|
+
throw new Error('db: transactions are not available in Supabase mode. Each statement runs as its ' +
|
|
51
|
+
'own PostgREST round-trip with no shared session, so `.transaction()` cannot provide ' +
|
|
52
|
+
'atomicity. Use connection-string mode (`db.connectionString`) if you need it.');
|
|
53
|
+
},
|
|
54
|
+
});
|
|
37
55
|
}
|
|
38
56
|
/**
|
|
39
57
|
* Runs a query as the **anonymous** role: no transaction, no role switch, no
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialises a JS value to a Postgres literal, mirroring how `pg` sends
|
|
3
|
+
* values over the wire so an inlined literal type-infers the same way a
|
|
4
|
+
* bound parameter would.
|
|
5
|
+
*
|
|
6
|
+
* Used to substitute `$n` placeholders client-side in Supabase mode, where
|
|
7
|
+
* `cfni_exec` takes a single already-complete statement — see
|
|
8
|
+
* {@link inlineParams}.
|
|
9
|
+
*/
|
|
10
|
+
export default function encodeParam(value: unknown): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialises a JS value to a Postgres literal, mirroring how `pg` sends
|
|
3
|
+
* values over the wire so an inlined literal type-infers the same way a
|
|
4
|
+
* bound parameter would.
|
|
5
|
+
*
|
|
6
|
+
* Used to substitute `$n` placeholders client-side in Supabase mode, where
|
|
7
|
+
* `cfni_exec` takes a single already-complete statement — see
|
|
8
|
+
* {@link inlineParams}.
|
|
9
|
+
*/
|
|
10
|
+
export default function encodeParam(value) {
|
|
11
|
+
if (value === null || value === undefined)
|
|
12
|
+
return 'NULL';
|
|
13
|
+
if (typeof value === 'boolean')
|
|
14
|
+
return value ? 'true' : 'false';
|
|
15
|
+
if (typeof value === 'number') {
|
|
16
|
+
if (Number.isNaN(value))
|
|
17
|
+
return "'NaN'";
|
|
18
|
+
if (value === Infinity)
|
|
19
|
+
return "'Infinity'";
|
|
20
|
+
if (value === -Infinity)
|
|
21
|
+
return "'-Infinity'";
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'bigint')
|
|
25
|
+
return value.toString();
|
|
26
|
+
if (value instanceof Date)
|
|
27
|
+
return quoteLiteral(value.toISOString());
|
|
28
|
+
if (value instanceof Uint8Array)
|
|
29
|
+
return quoteLiteral(`\\x${bytesToHex(value)}`);
|
|
30
|
+
if (Array.isArray(value))
|
|
31
|
+
return quoteLiteral(encodeArray(value));
|
|
32
|
+
if (typeof value === 'string')
|
|
33
|
+
return quoteLiteral(value);
|
|
34
|
+
// Plain objects (jsonb columns) — pg sends these JSON-stringified.
|
|
35
|
+
return quoteLiteral(JSON.stringify(value));
|
|
36
|
+
}
|
|
37
|
+
function quoteLiteral(text) {
|
|
38
|
+
return `'${text.replace(/'/g, "''")}'`;
|
|
39
|
+
}
|
|
40
|
+
function bytesToHex(bytes) {
|
|
41
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
42
|
+
}
|
|
43
|
+
/** Encodes a JS array as a Postgres array literal body, e.g. `{1,2,"a,b"}`. */
|
|
44
|
+
function encodeArray(value) {
|
|
45
|
+
const items = value.map((item) => {
|
|
46
|
+
if (item === null || item === undefined)
|
|
47
|
+
return 'NULL';
|
|
48
|
+
if (Array.isArray(item))
|
|
49
|
+
return encodeArray(item);
|
|
50
|
+
if (item instanceof Date)
|
|
51
|
+
return quoteArrayElement(item.toISOString());
|
|
52
|
+
if (typeof item === 'number' || typeof item === 'bigint' || typeof item === 'boolean')
|
|
53
|
+
return String(item);
|
|
54
|
+
return quoteArrayElement(typeof item === 'string' ? item : JSON.stringify(item));
|
|
55
|
+
});
|
|
56
|
+
return `{${items.join(',')}}`;
|
|
57
|
+
}
|
|
58
|
+
function quoteArrayElement(text) {
|
|
59
|
+
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
60
|
+
}
|
package/dist/src/db/helpers.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { sql, eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, type SQL, type Table } from 'drizzle-orm';
|
|
1
|
+
import { sql, eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, inArray, notInArray, ne, like, ilike, between, not, exists, type SQL, type Table } from 'drizzle-orm';
|
|
2
2
|
/**
|
|
3
3
|
* Re-exported `drizzle-orm` query-building primitives, so code that only
|
|
4
4
|
* calls `withPublicDb`/`withUserDb` and builds queries against the returned
|
|
5
5
|
* `DrizzleDb` never needs its own `drizzle-orm` import for common predicates,
|
|
6
6
|
* ordering, and aggregates.
|
|
7
7
|
*/
|
|
8
|
-
export { eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, sql };
|
|
8
|
+
export { eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, sql, inArray, notInArray, ne, like, ilike, between, not, exists };
|
|
9
9
|
/**
|
|
10
10
|
* Type-safe helper returning `excluded.<db_column_name>` SQL expressions for a Drizzle table.
|
|
11
11
|
*
|
package/dist/src/db/helpers.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { getTableColumns, getTableName, sql, eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, } from 'drizzle-orm';
|
|
1
|
+
import { getTableColumns, getTableName, sql, eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, inArray, notInArray, ne, like, ilike, between, not, exists, } from 'drizzle-orm';
|
|
2
2
|
/**
|
|
3
3
|
* Re-exported `drizzle-orm` query-building primitives, so code that only
|
|
4
4
|
* calls `withPublicDb`/`withUserDb` and builds queries against the returned
|
|
5
5
|
* `DrizzleDb` never needs its own `drizzle-orm` import for common predicates,
|
|
6
6
|
* ordering, and aggregates.
|
|
7
7
|
*/
|
|
8
|
-
export { eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, sql };
|
|
8
|
+
export { eq, and, or, asc, desc, gte, gt, lte, lt, isNull, isNotNull, count, sum, max, min, sql, inArray, notInArray, ne, like, ilike, between, not, exists };
|
|
9
9
|
/**
|
|
10
10
|
* Type-safe helper returning `excluded.<db_column_name>` SQL expressions for a Drizzle table.
|
|
11
11
|
*
|