better-supabase 0.2.0 → 0.4.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/README.md +37 -33
- package/dist/base64-D2gsioCY.js +42 -0
- package/dist/claims-BAf6c7is.js +48 -0
- package/dist/cli/_format-Cax5EsM9.js +101 -0
- package/dist/cli/bin.js +39 -6
- package/dist/cli/command-CK14fP91.js +502 -0
- package/dist/cli/compare-BmIWVEui.js +9 -0
- package/dist/cli/config-D9R0o1LH.js +1895 -0
- package/dist/cli/config-SSKSTUl2.js +2 -0
- package/dist/cli/db-DOBPt660.js +52 -0
- package/dist/cli/dist-B1MvLv-M.js +129 -0
- package/dist/cli/dist-CFgGknmZ.js +1112 -0
- package/dist/cli/dist-Du7VnEUG.js +621 -0
- package/dist/cli/doctor-BOym3mbL.js +2382 -0
- package/dist/cli/env-BZ0uy8XK.js +108 -0
- package/dist/cli/env-jIFUADLY.js +511 -0
- package/dist/cli/errors-4v4DTtda.js +56 -0
- package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
- package/dist/cli/gen-Ccn_p90L.js +2008 -0
- package/dist/cli/gen-D5cLCULx.js +2 -0
- package/dist/cli/index.d.ts +295 -28
- package/dist/cli/index.js +184 -3
- package/dist/cli/init-ClnxRYBF.js +550 -0
- package/dist/cli/introspect-DxqfkVIv.js +81 -0
- package/dist/cli/io-k41_z-jT.js +24 -0
- package/dist/cli/json5-ChRycquj.js +787 -0
- package/dist/cli/jsonc-BUniUpOk.js +2 -0
- package/dist/cli/jsonc-D5Fu67jw.js +45 -0
- package/dist/cli/keys-ByKHnvP9.js +83 -0
- package/dist/cli/ohash-D7bG_EOb.js +277 -0
- package/dist/cli/openapi-8WHjxiTJ.js +66 -0
- package/dist/cli/project-B_GeXyHh.js +70 -0
- package/dist/cli/prompts-BNcNfAve.js +1142 -0
- package/dist/cli/prompts-BTYQGunc.js +2 -0
- package/dist/cli/read-sets-CMa29t9a.js +971 -0
- package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
- package/dist/cli/run-CP20LKk_.js +347 -0
- package/dist/cli/seed-5J6eu_Xg.js +98 -0
- package/dist/cli/skills-LJJIhLFR.js +137 -0
- package/dist/cli/sql-D4VbqmXi.js +199 -0
- package/dist/cli/style-C0NFdJRL.js +29 -0
- package/dist/cli/toml-DB-MNnFs.js +424 -0
- package/dist/{version-Cqq1m3UY.js → cli/version-5mW_8sY1.js} +1 -1
- package/dist/cli/yaml-CYCbJ6et.js +2252 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +15 -25
- package/dist/{compiler-CZ9nIuqy.d.ts → compiler-DB3aSFOA.d.ts} +2 -2
- package/dist/config/index.d.ts +4 -3
- package/dist/config/index.js +5 -3
- package/dist/config-BpxDfnbg.js +129 -0
- package/dist/{index-DOsjFvMf.d.ts → config-C1CYcm6o.d.ts} +49 -49
- package/dist/{config-ClDXmVtY.js → config-CbvFmGD8.js} +14 -137
- package/dist/{define-BXMYD9AY.js → define-Bh0AqTik.js} +221 -120
- package/dist/{define-e3zI4_4g.d.ts → define-CRS2MRV3.d.ts} +36 -31
- package/dist/edge/index.d.ts +9 -13
- package/dist/edge/index.js +51 -25
- package/dist/{entitlements-Dng-ctuT.d.ts → entitlements-Dm-FHpNH.d.ts} +2 -2
- package/dist/env/index.js +203 -1
- package/dist/{errors-Bl3rkOzo.js → errors-L0xK41y_.js} +1 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.js +139 -1
- package/dist/{events-DOt3nSO-.d.ts → events-B21UOhvP.d.ts} +4 -4
- package/dist/{executor-CGdNQ27g.d.ts → executor-B5IoUxLE.d.ts} +2 -2
- package/dist/{executor-DeppvirK.js → executor-CVt6dD0B.js} +9 -4
- package/dist/{executor-BYXQ9AGn.d.ts → executor-CZ7ImRiD.d.ts} +2 -2
- package/dist/hono/index.d.ts +17 -24
- package/dist/hono/index.js +11 -11
- package/dist/{index-DuP14VHo.d.ts → index-528Ugmyp.d.ts} +30 -10
- package/dist/{index-Bw1bwtC0.d.ts → index-BAy4tsAP.d.ts} +28 -15
- package/dist/{index-CBDufH1M.d.ts → index-BBdvo-5d.d.ts} +24 -26
- package/dist/{index-DbxFr7XG.d.ts → index-BhHEC04r.d.ts} +9 -4
- package/dist/index-Cf-D2jk1.d.ts +50 -0
- package/dist/{index-Ciu5Xlml.d.ts → index-D5IfUIDh.d.ts} +6 -5
- package/dist/{index-K-3WM2Hp.d.ts → index-DFmOpBGC.d.ts} +13 -13
- package/dist/{index-D4v7sncu.d.ts → index-RlLXd8dL.d.ts} +13 -13
- package/dist/index.d.ts +17 -17
- package/dist/index.js +23 -14
- package/dist/{invalidate-DbxuL2jL.js → invalidate-CgxmRamI.js} +7 -1
- package/dist/jobs/index.d.ts +33 -26
- package/dist/jobs/index.js +44 -22
- package/dist/{json-schema-BpYqkCKf.js → json-schema-DP6V7cbr.js} +8 -3
- package/dist/lint/index.js +1 -1
- package/dist/list/index.d.ts +2 -2
- package/dist/list/index.js +131 -96
- package/dist/{live-DJyZMD6w.d.ts → live-BHakSWWo.d.ts} +26 -16
- package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
- package/dist/lookup-BaP5bI27.js +18 -0
- package/dist/mcp/index.d.ts +34 -27
- package/dist/mcp/index.js +59 -25
- package/dist/{mfa-TVia554c.d.ts → mfa-C7MNQ1Ob.d.ts} +1 -1
- package/dist/{mfa-CpLfJSde.js → mfa-byfjruR9.js} +190 -46
- package/dist/next/image/index.js +6 -3
- package/dist/next/index.d.ts +64 -48
- package/dist/next/index.js +63 -50
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +61 -38
- package/dist/orpc/index.d.ts +18 -21
- package/dist/orpc/index.js +12 -12
- package/dist/otel/index.d.ts +5 -5
- package/dist/otel/index.js +84 -63
- package/dist/{permdock-sql-CghlHPuH.js → permdock-sql-C4KFzLc9.js} +12 -7
- package/dist/permdock-sql-DImJULY-.d.ts +17 -0
- package/dist/{plugin-C4TR9J0Z.d.ts → plugin-bNhvJwRh.d.ts} +5 -4
- package/dist/plugins/actor/index.d.ts +1 -1
- package/dist/plugins/actor/index.js +1 -1
- package/dist/plugins/rules/index.d.ts +2 -2
- package/dist/plugins/rules/index.js +46 -16
- package/dist/plugins/soft-delete/index.d.ts +3 -3
- package/dist/plugins/soft-delete/index.js +4 -4
- package/dist/plugins/tenant/index.d.ts +3 -3
- package/dist/plugins/tenant/index.js +10 -4
- package/dist/plugins/timestamps/index.d.ts +1 -1
- package/dist/plugins/timestamps/index.js +2 -2
- package/dist/plugins/validation/index.d.ts +2 -2
- package/dist/plugins/validation/index.js +3 -3
- package/dist/pool-DFwEQwmN.d.ts +78 -0
- package/dist/postgres/index.d.ts +10 -5
- package/dist/postgres/index.js +37 -16
- package/dist/{postgrest-cAEQ_IQN.d.ts → postgrest-Bc-0Xw62.d.ts} +1 -1
- package/dist/{problem-BTkTtRV9.js → problem-BihjPR0t.js} +11 -5
- package/dist/{problem-DlgB0lYl.d.ts → problem-U8UEKccv.d.ts} +6 -4
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +2 -2
- package/dist/{query-ChvDbsDE.js → query-BMMSWbI7.js} +19 -19
- package/dist/react/index.d.ts +7 -7
- package/dist/react/index.js +2 -2
- package/dist/react/server.d.ts +6 -6
- package/dist/react/server.js +3 -3
- package/dist/react/session.d.ts +21 -2
- package/dist/react/session.js +19 -1
- package/dist/{react-C83BdBGt.js → react-CQHMaESR.js} +59 -55
- package/dist/{read-set-4lI3W_gZ.js → read-set-FWK8qQ58.js} +51 -16
- package/dist/realtime/index.d.ts +4 -3
- package/dist/realtime/index.js +19 -17
- package/dist/{index-CG69s15v.d.ts → refresh-BwYkyNOQ.d.ts} +7 -3
- package/dist/{resolve-CjHILk2w.d.ts → resolve-BAqQcwMW.d.ts} +17 -11
- package/dist/{resource-CgZa3YG_.d.ts → resource-BJcaIfxi.d.ts} +17 -8
- package/dist/{resource-BHEWmQJK.js → resource-My_y-Syh.js} +43 -21
- package/dist/{respond-Dne1ARSa.d.ts → respond-DgIbdrt8.d.ts} +39 -14
- package/dist/{result-DWXatkd6.js → result-Bj6yt33D.js} +2 -2
- package/dist/{result-CTkCZ6JA.d.ts → result-CqEsbgYs.d.ts} +16 -11
- package/dist/server/index.d.ts +38 -13
- package/dist/server/index.js +43 -14
- package/dist/{respond-DMcbQwfN.js → server-BA75B0mT.js} +151 -96
- package/dist/{shared-_6-uPL7E.js → shared-DnoCdMDr.js} +4 -3
- package/dist/{simplify-bwJpLnaS.js → simplify-Cqvfr9dj.js} +76 -39
- package/dist/sql/index.d.ts +139 -0
- package/dist/sql/index.js +1983 -0
- package/dist/{sql-DTjA1LPj.js → sql-B34HU2DI.js} +74 -17
- package/dist/ssr/index.d.ts +4 -4
- package/dist/ssr/index.js +2 -2
- package/dist/{standard-FUi3Z3-7.js → standard-BRyBGTtp.js} +2 -2
- package/dist/{standard-CyhRzctb.d.ts → standard-BsaCYQJc.d.ts} +1 -1
- package/dist/{stats-C5W-rBfR.js → stats-C_YxA2r1.js} +40 -14
- package/dist/storage/index.d.ts +4 -3
- package/dist/storage/index.js +548 -1
- package/dist/{tables-rZxRxNPW.js → tables-Cb-3_VWc.js} +19 -5
- package/dist/temporal-BwshQk0W.js +27 -0
- package/dist/temporal-required-CBNHT6ni.js +19 -0
- package/dist/testing/index.d.ts +25 -24
- package/dist/testing/index.js +165 -66
- package/dist/{types-C83v5SQz.d.ts → types-DONv9SHN.d.ts} +14 -11
- package/dist/version-Bmf7_FzM.js +4 -0
- package/dist/{view-D1s0wa0T.d.ts → view-B64o81wp.d.ts} +36 -6
- package/dist/{view-aOYizTeE.js → view-BrJI2LKp.js} +11 -2
- package/dist/webhooks/index.d.ts +19 -17
- package/dist/webhooks/index.js +1 -1
- package/dist/{webhooks-DMipjhSP.js → webhooks-Bh_Jf_-l.js} +45 -24
- package/package.json +50 -22
- package/schemas/config-v1.json +31 -1
- package/schemas/doctor-report-v1.json +17 -1
- package/schemas/snapshot-v2.json +35 -0
- package/skills/better-supabase/SKILL.md +77 -18
- package/skills/better-supabase/references/plugins.md +21 -22
- package/skills/better-supabase/references/troubleshooting.md +38 -18
- package/skills/better-supabase-api/SKILL.md +66 -18
- package/skills/better-supabase-api/references/adapters.md +64 -41
- package/skills/better-supabase-auth/SKILL.md +146 -0
- package/skills/better-supabase-auth/references/permdock.md +55 -0
- package/skills/better-supabase-testing/SKILL.md +68 -11
- package/dist/casing-da0uRTqt.js +0 -14
- package/dist/claims-CoTobNC-.js +0 -25
- package/dist/cli-DrSgDOQC.js +0 -6646
- package/dist/env-BEToab0I.js +0 -204
- package/dist/events-BuzTgn2D.js +0 -135
- package/dist/pool-R2m_DkXi.d.ts +0 -37
- package/dist/seed-CzecODcJ.js +0 -101
- package/dist/session-C3LxnQ0e.d.ts +0 -22
- package/dist/session-CqEwzVup.js +0 -20
- package/dist/storage-DuGn38fr.js +0 -527
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { t as SqlClient } from "../executor-
|
|
7
|
-
import { t as
|
|
8
|
-
import { n as AuthResolver } from "../resolve-
|
|
9
|
-
import { r as EventSink, t as CloudEvent } from "../index-
|
|
1
|
+
import { A as SchemaMeta, N as TableKey, R as Update, g as Insert, n as AnyModels, t as AnyFunctions } from "../types-DONv9SHN.js";
|
|
2
|
+
import { r as Executor } from "../executor-B5IoUxLE.js";
|
|
3
|
+
import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-CRS2MRV3.js";
|
|
4
|
+
import { h as RequestContext, n as AnyPlugin } from "../plugin-bNhvJwRh.js";
|
|
5
|
+
import { b as ResolvedConfig, d as Generator, f as GeneratorInput } from "../config-C1CYcm6o.js";
|
|
6
|
+
import { t as SqlClient } from "../executor-CZ7ImRiD.js";
|
|
7
|
+
import { t as BetterPostgres } from "../pool-DFwEQwmN.js";
|
|
8
|
+
import { n as AuthResolver } from "../resolve-BAqQcwMW.js";
|
|
9
|
+
import { r as EventSink, t as CloudEvent } from "../index-D5IfUIDh.js";
|
|
10
10
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
11
|
+
import { webcrypto } from "node:crypto";
|
|
11
12
|
//#region src/testing/jwt.d.ts
|
|
12
13
|
interface TestJwtClaims {
|
|
13
14
|
readonly sub: string;
|
|
@@ -24,19 +25,19 @@ interface TestJwtClaims {
|
|
|
24
25
|
export declare function signTestJwt(secret: string, claims: TestJwtClaims): Promise<string>;
|
|
25
26
|
/**
|
|
26
27
|
* Accepts `signTestJwt` tokens as users, for API servers tested against the
|
|
27
|
-
* local stack: `createServer(
|
|
28
|
+
* local stack: `createServer(betterSupabase, { auth: { resolvers: [localAuth(secret)] } })`.
|
|
28
29
|
* Tokens that are not HS256-signed with `secret` fall through. Test-only.
|
|
29
30
|
*/
|
|
30
31
|
export declare function localAuth(secret: string): AuthResolver;
|
|
31
32
|
interface TestSigner {
|
|
32
33
|
/** Public JWKS; pass as `jwks` to `resolveAuth` / `createServer({ auth: { jwks } })`. */
|
|
33
34
|
readonly jwks: {
|
|
34
|
-
readonly keys: readonly JsonWebKey[];
|
|
35
|
+
readonly keys: readonly webcrypto.JsonWebKey[];
|
|
35
36
|
};
|
|
36
37
|
sign(claims: TestJwtClaims): Promise<string>;
|
|
37
38
|
}
|
|
38
39
|
/** A private ES256 JWK, as `better-supabase keys` writes to `supabase/signing_keys.json`. */
|
|
39
|
-
interface SigningJwk extends JsonWebKey {
|
|
40
|
+
interface SigningJwk extends webcrypto.JsonWebKey {
|
|
40
41
|
readonly kid: string;
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
@@ -80,7 +81,7 @@ interface LocalStack {
|
|
|
80
81
|
/** Service-role key for `expectTenantIsolation`; defaults to `$SUPABASE_SECRET_KEY`. */
|
|
81
82
|
readonly secretKey?: string;
|
|
82
83
|
/** Enables `sql`: the same user over direct Postgres. */
|
|
83
|
-
readonly postgres?:
|
|
84
|
+
readonly postgres?: BetterPostgres;
|
|
84
85
|
}
|
|
85
86
|
interface TestUser<M extends AnyModels, F extends AnyFunctions, E> {
|
|
86
87
|
readonly token: string;
|
|
@@ -99,11 +100,11 @@ interface TestUser<M extends AnyModels, F extends AnyFunctions, E> {
|
|
|
99
100
|
* still uses the shared JWT secret.
|
|
100
101
|
*
|
|
101
102
|
* ```ts
|
|
102
|
-
* const alice = await asUser(
|
|
103
|
+
* const alice = await asUser(betterSupabase, { sub: aliceId, tenant_id: acme });
|
|
103
104
|
* expect(await alice.db.customers.count().orThrow()).toBe(3);
|
|
104
105
|
* ```
|
|
105
106
|
*/
|
|
106
|
-
export declare function asUser<M extends AnyModels, D, F extends AnyFunctions, E>(
|
|
107
|
+
export declare function asUser<M extends AnyModels, D, F extends AnyFunctions, E>(betterSupabase: BetterSupabase<M, D, F, E>, claims: TestJwtClaims, stack?: LocalStack): Promise<TestUser<M, F, E>>;
|
|
107
108
|
//#endregion
|
|
108
109
|
//#region src/testing/budget.d.ts
|
|
109
110
|
/** The part of a Playwright `Response` the budget check reads. */
|
|
@@ -146,7 +147,7 @@ interface MeasuredRender {
|
|
|
146
147
|
/**
|
|
147
148
|
* Measures every document and RSC navigation response during `during` and
|
|
148
149
|
* fails when one render makes more database calls or waves than allowed.
|
|
149
|
-
* Needs `createNext(
|
|
150
|
+
* Needs `createNext(betterSupabase, { debug })` and `bs.debugRoute()` in the app.
|
|
150
151
|
*/
|
|
151
152
|
export declare function expectDbBudget(page: BudgetPage, expectation: DbBudgetExpectation): Promise<readonly MeasuredRender[]>;
|
|
152
153
|
//#endregion
|
|
@@ -169,7 +170,7 @@ export declare class ConformanceError extends Error {
|
|
|
169
170
|
type AnySupabase = BetterSupabase<any, any, any, any>;
|
|
170
171
|
interface TestExecutorOptions {
|
|
171
172
|
/** A definition whose schema matches the executor's database. Install no plugins. */
|
|
172
|
-
readonly
|
|
173
|
+
readonly betterSupabase: AnySupabase;
|
|
173
174
|
/** App key of a table with rows the executor can read. Defaults to the first table. */
|
|
174
175
|
readonly table?: string;
|
|
175
176
|
/** A row to insert and delete again, to check writes. Skips write checks when omitted. */
|
|
@@ -181,7 +182,7 @@ interface TestExecutorOptions {
|
|
|
181
182
|
* than thrown, and (with `create`) a write round trip.
|
|
182
183
|
*
|
|
183
184
|
* ```ts
|
|
184
|
-
* it('conforms', () => testExecutor(myExecutor, {
|
|
185
|
+
* it('conforms', () => testExecutor(myExecutor, { betterSupabase, table: 'tags', create: { name: 'x' } }));
|
|
185
186
|
* ```
|
|
186
187
|
*/
|
|
187
188
|
export declare function testExecutor(executor: Executor, options: TestExecutorOptions): Promise<ConformanceReport>;
|
|
@@ -226,7 +227,7 @@ interface TestGeneratorOptions {
|
|
|
226
227
|
export declare function testGenerator(generator: Generator, options: TestGeneratorOptions): Promise<ConformanceReport>;
|
|
227
228
|
interface TestPluginOptions {
|
|
228
229
|
/** The definition without this plugin. */
|
|
229
|
-
readonly
|
|
230
|
+
readonly betterSupabase: AnySupabase;
|
|
230
231
|
/** Table to exercise. Defaults to the first table. */
|
|
231
232
|
readonly table?: string;
|
|
232
233
|
/** Context the plugin needs, such as `{ tenant }` for `tenant()`. */
|
|
@@ -271,12 +272,12 @@ interface TenantIsolationOptions<M extends AnyModels> {
|
|
|
271
272
|
/**
|
|
272
273
|
* Checks row-level security between two tenants against a running stack: for
|
|
273
274
|
* each table, a user of one tenant must not select, insert, update or delete
|
|
274
|
-
* the other tenant's rows. Runs without `
|
|
275
|
+
* the other tenant's rows. Runs without `betterSupabase`'s plugins, so the `tenant()`
|
|
275
276
|
* plugin can't hide a missing policy. Throws a `ConformanceError` naming each
|
|
276
277
|
* table and command that leaks; removes every row it created.
|
|
277
278
|
*
|
|
278
279
|
* ```ts
|
|
279
|
-
* await expectTenantIsolation(
|
|
280
|
+
* await expectTenantIsolation(betterSupabase, {
|
|
280
281
|
* tenants: [
|
|
281
282
|
* { id: ACME, claims: { sub: alice, tenant_id: ACME } },
|
|
282
283
|
* { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
|
|
@@ -290,7 +291,7 @@ interface TenantIsolationOptions<M extends AnyModels> {
|
|
|
290
291
|
* });
|
|
291
292
|
* ```
|
|
292
293
|
*/
|
|
293
|
-
export declare function expectTenantIsolation<M extends AnyModels, D, F extends AnyFunctions, E>(
|
|
294
|
+
export declare function expectTenantIsolation<M extends AnyModels, D, F extends AnyFunctions, E>(betterSupabase: BetterSupabase<M, D, F, E>, options: TenantIsolationOptions<M>): Promise<ConformanceReport>;
|
|
294
295
|
//#endregion
|
|
295
296
|
//#region src/testing/seed.d.ts
|
|
296
297
|
/** Named rows per table, in app casing: `{ customers: { acme: { ... } } }`. */
|
|
@@ -314,12 +315,12 @@ export declare function isSeed(value: unknown): value is Seed<unknown>;
|
|
|
314
315
|
* casing and Insert types; `better-supabase seed` renders them to SQL.
|
|
315
316
|
*
|
|
316
317
|
* ```ts
|
|
317
|
-
* export const seed = defineSeed(
|
|
318
|
+
* export const seed = defineSeed(betterSupabase, {
|
|
318
319
|
* organizations: { acme: { id: ACME, name: 'Acme' } },
|
|
319
320
|
* customers: { first: { organizationId: ACME, name: 'First customer' } },
|
|
320
321
|
* });
|
|
321
322
|
* ```
|
|
322
323
|
*/
|
|
323
|
-
export declare function defineSeed<M extends AnyModels, D, F extends AnyFunctions, E, const S extends SeedFixtures<M>>(
|
|
324
|
+
export declare function defineSeed<M extends AnyModels, D, F extends AnyFunctions, E, const S extends SeedFixtures<M>>(betterSupabase: BetterSupabase<M, D, F, E>, fixtures: S & ExactSeed<M, S>): Seed<S>;
|
|
324
325
|
//#endregion
|
|
325
326
|
export type { BudgetPage, BudgetResponse, ConformanceCheck, ConformanceReport, DbBudgetExpectation, ExactSeed, IsolationTable, IsolationTables, IsolationTenant, LocalStack, MeasuredRender, Seed, SeedFixtures, SigningJwk, TenantIsolationOptions, TestAuthResolverOptions, TestEventSinkOptions, TestExecutorOptions, TestGeneratorOptions, TestJwtClaims, TestPluginOptions, TestSigner, TestUser };
|
package/dist/testing/index.js
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
import { n as dbError } from "../errors-
|
|
2
|
-
import { i as
|
|
3
|
-
import { t as
|
|
1
|
+
import { n as dbError } from "../errors-L0xK41y_.js";
|
|
2
|
+
import { i as temporalText } from "../temporal-BwshQk0W.js";
|
|
3
|
+
import { t as temporal } from "../temporal-required-CBNHT6ni.js";
|
|
4
|
+
import { i as ok } from "../result-Bj6yt33D.js";
|
|
5
|
+
import { t as BetterSupabase } from "../define-Bh0AqTik.js";
|
|
6
|
+
import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-D2gsioCY.js";
|
|
7
|
+
import { r as resolveConfig } from "../config-BpxDfnbg.js";
|
|
4
8
|
import { t as isList } from "../guards-BnzDvFrz.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { o as toCloudEvents } from "../events-BuzTgn2D.js";
|
|
9
|
-
import { n as isSeed, t as defineSeed } from "../seed-CzecODcJ.js";
|
|
9
|
+
import { ENV_VARIABLES } from "../env/index.js";
|
|
10
|
+
import { n as postgresExecutor } from "../executor-CVt6dD0B.js";
|
|
11
|
+
import { toCloudEvents } from "../events/index.js";
|
|
10
12
|
import { createClient } from "@supabase/supabase-js";
|
|
11
13
|
import { existsSync } from "node:fs";
|
|
12
14
|
import { readFile } from "node:fs/promises";
|
|
13
15
|
import { dirname, join, resolve } from "node:path";
|
|
14
16
|
//#region src/testing/jwt.ts
|
|
15
|
-
const encoder = new TextEncoder();
|
|
16
|
-
function base64url(input) {
|
|
17
|
-
const bytes = typeof input === "string" ? encoder.encode(input) : input;
|
|
18
|
-
let binary = "";
|
|
19
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
20
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
21
|
-
}
|
|
17
|
+
const encoder = /* @__PURE__ */ new TextEncoder();
|
|
22
18
|
/**
|
|
23
19
|
* Signs an HS256 access token the local Supabase stack accepts. Test-only:
|
|
24
20
|
* production tokens come from Supabase Auth.
|
|
@@ -34,25 +30,21 @@ async function signTestJwt(secret, claims) {
|
|
|
34
30
|
exp: now + expiresIn,
|
|
35
31
|
...rest
|
|
36
32
|
};
|
|
37
|
-
const header =
|
|
33
|
+
const header = toBase64Url(JSON.stringify({
|
|
38
34
|
alg: "HS256",
|
|
39
35
|
typ: "JWT"
|
|
40
36
|
}));
|
|
41
|
-
const body =
|
|
37
|
+
const body = toBase64Url(JSON.stringify(payload));
|
|
42
38
|
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
43
39
|
name: "HMAC",
|
|
44
40
|
hash: "SHA-256"
|
|
45
41
|
}, false, ["sign"]);
|
|
46
42
|
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${header}.${body}`));
|
|
47
|
-
return `${header}.${body}.${
|
|
48
|
-
}
|
|
49
|
-
function base64urlDecode(input) {
|
|
50
|
-
const binary = atob(input.replaceAll("-", "+").replaceAll("_", "/"));
|
|
51
|
-
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
43
|
+
return `${header}.${body}.${toBase64Url(new Uint8Array(signature))}`;
|
|
52
44
|
}
|
|
53
45
|
/**
|
|
54
46
|
* Accepts `signTestJwt` tokens as users, for API servers tested against the
|
|
55
|
-
* local stack: `createServer(
|
|
47
|
+
* local stack: `createServer(betterSupabase, { auth: { resolvers: [localAuth(secret)] } })`.
|
|
56
48
|
* Tokens that are not HS256-signed with `secret` fall through. Test-only.
|
|
57
49
|
*/
|
|
58
50
|
function localAuth(secret) {
|
|
@@ -65,9 +57,9 @@ function localAuth(secret) {
|
|
|
65
57
|
async resolve(request) {
|
|
66
58
|
const token = /^Bearer (.+)$/i.exec(request.headers.get("authorization") ?? "")?.[1];
|
|
67
59
|
const [header, body, signature] = token?.split(".") ?? [];
|
|
68
|
-
if (!token || !header || !body || !signature) return
|
|
69
|
-
if (!await crypto.subtle.verify("HMAC", await key,
|
|
70
|
-
const claims = JSON.parse(
|
|
60
|
+
if (!token || !header || !body || !signature) return;
|
|
61
|
+
if (!await crypto.subtle.verify("HMAC", await key, fromBase64(signature), encoder.encode(`${header}.${body}`)).catch(() => false)) return;
|
|
62
|
+
const claims = JSON.parse(base64ToText(body));
|
|
71
63
|
if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) return {
|
|
72
64
|
kind: "invalid",
|
|
73
65
|
error: dbError("unauthorized", "The token has expired")
|
|
@@ -98,17 +90,17 @@ async function signEs256(key, kid, claims) {
|
|
|
98
90
|
exp: now + expiresIn,
|
|
99
91
|
...rest
|
|
100
92
|
};
|
|
101
|
-
const header =
|
|
93
|
+
const header = toBase64Url(JSON.stringify({
|
|
102
94
|
alg: "ES256",
|
|
103
95
|
typ: "JWT",
|
|
104
96
|
kid
|
|
105
97
|
}));
|
|
106
|
-
const body =
|
|
98
|
+
const body = toBase64Url(JSON.stringify(payload));
|
|
107
99
|
const signature = await crypto.subtle.sign({
|
|
108
100
|
name: "ECDSA",
|
|
109
101
|
hash: "SHA-256"
|
|
110
102
|
}, key, encoder.encode(`${header}.${body}`));
|
|
111
|
-
return `${header}.${body}.${
|
|
103
|
+
return `${header}.${body}.${toBase64Url(new Uint8Array(signature))}`;
|
|
112
104
|
}
|
|
113
105
|
/**
|
|
114
106
|
* Signs an ES256 access token with a private JWK, the way the local stack
|
|
@@ -206,11 +198,11 @@ async function signFor(stack, url, claims) {
|
|
|
206
198
|
* still uses the shared JWT secret.
|
|
207
199
|
*
|
|
208
200
|
* ```ts
|
|
209
|
-
* const alice = await asUser(
|
|
201
|
+
* const alice = await asUser(betterSupabase, { sub: aliceId, tenant_id: acme });
|
|
210
202
|
* expect(await alice.db.customers.count().orThrow()).toBe(3);
|
|
211
203
|
* ```
|
|
212
204
|
*/
|
|
213
|
-
async function asUser(
|
|
205
|
+
async function asUser(betterSupabase, claims, stack = {}) {
|
|
214
206
|
const url = stack.url ?? env$1(...ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
|
|
215
207
|
const publishableKey = stack.publishableKey ?? env$1(...ENV_VARIABLES.publishableKey, "SUPABASE_ANON_KEY");
|
|
216
208
|
if (!publishableKey) throw new TypeError("asUser needs publishableKey or $SUPABASE_PUBLISHABLE_KEY. Run `better-supabase env` to write it to .env.local.");
|
|
@@ -233,8 +225,8 @@ async function asUser(sb, claims, stack = {}) {
|
|
|
233
225
|
token,
|
|
234
226
|
claims: full,
|
|
235
227
|
supabase,
|
|
236
|
-
db:
|
|
237
|
-
sql: stack.postgres ?
|
|
228
|
+
db: betterSupabase.connect(supabase, context),
|
|
229
|
+
sql: stack.postgres ? betterSupabase.connect(postgresExecutor(stack.postgres.asUser(full)), context) : void 0
|
|
238
230
|
};
|
|
239
231
|
}
|
|
240
232
|
//#endregion
|
|
@@ -247,12 +239,14 @@ function settle(promise, ms) {
|
|
|
247
239
|
let timer;
|
|
248
240
|
return Promise.race([promise, new Promise((done) => {
|
|
249
241
|
timer = setTimeout(done, ms);
|
|
250
|
-
})]).finally(() =>
|
|
242
|
+
})]).finally(() => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
});
|
|
251
245
|
}
|
|
252
246
|
/**
|
|
253
247
|
* Measures every document and RSC navigation response during `during` and
|
|
254
248
|
* fails when one render makes more database calls or waves than allowed.
|
|
255
|
-
* Needs `createNext(
|
|
249
|
+
* Needs `createNext(betterSupabase, { debug })` and `bs.debugRoute()` in the app.
|
|
256
250
|
*/
|
|
257
251
|
async function expectDbBudget(page, expectation) {
|
|
258
252
|
const seen = [];
|
|
@@ -273,12 +267,12 @@ async function expectDbBudget(page, expectation) {
|
|
|
273
267
|
} finally {
|
|
274
268
|
page.off("response", listener);
|
|
275
269
|
}
|
|
276
|
-
if (seen.length === 0) throw new Error("expectDbBudget: no response carried x-bs-request-id. Enable createNext(
|
|
270
|
+
if (seen.length === 0) throw new Error("expectDbBudget: no response carried x-bs-request-id. Enable createNext(betterSupabase, { debug: { enabled: true } }) and run the proxy.");
|
|
277
271
|
await Promise.all(seen.map(({ response }) => settle(response.finished(), expectation.timeoutMs ?? 1e4)));
|
|
278
272
|
const renders = [];
|
|
279
273
|
for (const { response, id, url } of seen) {
|
|
280
274
|
const reply = await page.request.get(url);
|
|
281
|
-
if (!reply.ok()) throw new Error(`expectDbBudget: ${url} answered ${reply.status()}. Mount
|
|
275
|
+
if (!reply.ok()) throw new Error(`expectDbBudget: ${url} answered ${reply.status()}. Mount bs.debugRoute() at that path.`);
|
|
282
276
|
renders.push({
|
|
283
277
|
url: response.url(),
|
|
284
278
|
requestId: id,
|
|
@@ -354,34 +348,36 @@ const recorder = (ops) => ({
|
|
|
354
348
|
}));
|
|
355
349
|
}
|
|
356
350
|
});
|
|
357
|
-
/** The operation `run` sends to the executor, built by `
|
|
358
|
-
async function captureOp(
|
|
351
|
+
/** The operation `run` sends to the executor, built by `betterSupabase` without touching a database. */
|
|
352
|
+
async function captureOp(betterSupabase, run) {
|
|
359
353
|
const ops = [];
|
|
360
|
-
await run(
|
|
354
|
+
await run(betterSupabase.connect(recorder(ops)));
|
|
361
355
|
const [op] = ops;
|
|
362
356
|
if (!op) throw new Error("the repository call did not reach the executor");
|
|
363
357
|
return op;
|
|
364
358
|
}
|
|
365
|
-
function tableKey(
|
|
366
|
-
const key = table ?? Object.keys(
|
|
367
|
-
if (!key || !(key in
|
|
359
|
+
function tableKey(betterSupabase, table) {
|
|
360
|
+
const key = table ?? Object.keys(betterSupabase.meta.tables)[0];
|
|
361
|
+
if (!key || !(key in betterSupabase.meta.tables)) throw new TypeError(`better-supabase: unknown table "${String(key)}"`);
|
|
368
362
|
return key;
|
|
369
363
|
}
|
|
370
|
-
const hasName = (subject) => ["has a name", () =>
|
|
364
|
+
const hasName = (subject) => ["has a name", () => {
|
|
365
|
+
expect(typeof subject.name === "string" && subject.name.length > 0, "name must be a non-empty string");
|
|
366
|
+
}];
|
|
371
367
|
/**
|
|
372
368
|
* Proves an `Executor` meets the contract: rows keyed by the selection's
|
|
373
369
|
* aliases, counts, `aborted` errors, failures returned as `Result`s rather
|
|
374
370
|
* than thrown, and (with `create`) a write round trip.
|
|
375
371
|
*
|
|
376
372
|
* ```ts
|
|
377
|
-
* it('conforms', () => testExecutor(myExecutor, {
|
|
373
|
+
* it('conforms', () => testExecutor(myExecutor, { betterSupabase, table: 'tags', create: { name: 'x' } }));
|
|
378
374
|
* ```
|
|
379
375
|
*/
|
|
380
376
|
function testExecutor(executor, options) {
|
|
381
|
-
const {
|
|
382
|
-
const table = tableKey(
|
|
377
|
+
const { betterSupabase } = options;
|
|
378
|
+
const table = tableKey(betterSupabase, options.table);
|
|
383
379
|
const context = { errorMappers: [] };
|
|
384
|
-
const read = () => captureOp(
|
|
380
|
+
const read = () => captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 2 }));
|
|
385
381
|
const create = options.create;
|
|
386
382
|
return conform(`Executor "${executor.name}"`, [
|
|
387
383
|
hasName(executor),
|
|
@@ -400,7 +396,7 @@ function testExecutor(executor, options) {
|
|
|
400
396
|
}
|
|
401
397
|
}],
|
|
402
398
|
["counts rows", async () => {
|
|
403
|
-
const op = await captureOp(
|
|
399
|
+
const op = await captureOp(betterSupabase, (db) => db[table]["count"]());
|
|
404
400
|
const result = await executor.execute(op, context);
|
|
405
401
|
expect(result.ok, `count failed: ${result.ok ? "" : result.error.message}`);
|
|
406
402
|
expect(typeof result.data.count === "number" && result.data.count >= 0, "count must be a number");
|
|
@@ -448,7 +444,7 @@ function testExecutor(executor, options) {
|
|
|
448
444
|
}],
|
|
449
445
|
executor.batch && ["batch returns one result per operation, in order", async () => {
|
|
450
446
|
const rows = await read();
|
|
451
|
-
const count = await captureOp(
|
|
447
|
+
const count = await captureOp(betterSupabase, (db) => db[table]["count"]());
|
|
452
448
|
const missing = {
|
|
453
449
|
...rows,
|
|
454
450
|
table: {
|
|
@@ -468,12 +464,12 @@ function testExecutor(executor, options) {
|
|
|
468
464
|
expect(third?.ok === false, "a failing operation must fail on its own, as a result");
|
|
469
465
|
}],
|
|
470
466
|
create && ["round-trips a write", async () => {
|
|
471
|
-
const repository =
|
|
467
|
+
const repository = betterSupabase.connect(executor)[table];
|
|
472
468
|
const created = await repository["create"](create);
|
|
473
469
|
expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
|
|
474
470
|
const row = created.data;
|
|
475
471
|
for (const [key, value] of Object.entries(create)) expect(same(row[key], value), `created row has ${key} = ${JSON.stringify(row[key])}`);
|
|
476
|
-
const [primary] =
|
|
472
|
+
const [primary] = betterSupabase.meta.tables[table].primaryKey;
|
|
477
473
|
expect(primary, "write checks need a table with a primary key");
|
|
478
474
|
const removed = await repository["delete"](row[primary]);
|
|
479
475
|
expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
|
|
@@ -647,33 +643,35 @@ function testGenerator(generator, options) {
|
|
|
647
643
|
* results intact when it wraps them.
|
|
648
644
|
*/
|
|
649
645
|
function testPlugin(plugin, options) {
|
|
650
|
-
const {
|
|
651
|
-
const table = tableKey(
|
|
646
|
+
const { betterSupabase } = options;
|
|
647
|
+
const table = tableKey(betterSupabase, options.table);
|
|
652
648
|
const context = options.context ?? {};
|
|
653
649
|
const hook = (op) => ({
|
|
654
650
|
table: op.table,
|
|
655
|
-
schema:
|
|
651
|
+
schema: betterSupabase.meta,
|
|
656
652
|
context,
|
|
657
653
|
options: {},
|
|
658
|
-
now: () =>
|
|
654
|
+
now: () => temporal().Instant.fromEpochMilliseconds(0)
|
|
659
655
|
});
|
|
660
656
|
const create = options.create;
|
|
661
657
|
return conform(`Plugin "${plugin.name}"`, [
|
|
662
658
|
hasName(plugin),
|
|
663
|
-
["targets plugin API v1", () =>
|
|
659
|
+
["targets plugin API v1", () => {
|
|
660
|
+
expect(plugin.apiVersion === 1, `apiVersion is ${String(plugin.apiVersion)}`);
|
|
661
|
+
}],
|
|
664
662
|
["installs and builds repositories", () => {
|
|
665
|
-
const db =
|
|
666
|
-
for (const key of Object.keys(
|
|
663
|
+
const db = betterSupabase.use(plugin).connect(recorder([]), context);
|
|
664
|
+
for (const key of Object.keys(betterSupabase.meta.tables)) expect(db[key], `db.${key} is missing`);
|
|
667
665
|
}],
|
|
668
666
|
plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
|
|
669
|
-
const op = await captureOp(
|
|
667
|
+
const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
|
|
670
668
|
const first = plugin.transformQuery(frozenCopy(op), hook(op));
|
|
671
669
|
const second = plugin.transformQuery(frozenCopy(op), hook(op));
|
|
672
670
|
expect(typeof first === "object" && first !== null && first.kind === op.kind, "must return an operation of the same kind");
|
|
673
671
|
expect(same(first, second), "two calls with the same input differ");
|
|
674
672
|
}],
|
|
675
673
|
plugin.beforeMutation && create && ["beforeMutation is pure and deterministic", async () => {
|
|
676
|
-
const op = await captureOp(
|
|
674
|
+
const op = await captureOp(betterSupabase, (db) => db[table]["create"](create));
|
|
677
675
|
if (op.kind === "select") throw new Violation("create did not produce a mutation");
|
|
678
676
|
const first = await plugin.beforeMutation(frozenCopy(op), hook(op));
|
|
679
677
|
const second = await plugin.beforeMutation(frozenCopy(op), hook(op));
|
|
@@ -681,7 +679,7 @@ function testPlugin(plugin, options) {
|
|
|
681
679
|
expect(same(first, second), "two calls with the same input differ");
|
|
682
680
|
}],
|
|
683
681
|
plugin.wrapExecutor && ["wrapExecutor keeps results intact", async () => {
|
|
684
|
-
const op = await captureOp(
|
|
682
|
+
const op = await captureOp(betterSupabase, (db) => db[table]["findMany"]({ limit: 1 }));
|
|
685
683
|
const data = {
|
|
686
684
|
rows: [{ marker: "conformance" }],
|
|
687
685
|
count: 1
|
|
@@ -725,12 +723,12 @@ function env(names) {
|
|
|
725
723
|
/**
|
|
726
724
|
* Checks row-level security between two tenants against a running stack: for
|
|
727
725
|
* each table, a user of one tenant must not select, insert, update or delete
|
|
728
|
-
* the other tenant's rows. Runs without `
|
|
726
|
+
* the other tenant's rows. Runs without `betterSupabase`'s plugins, so the `tenant()`
|
|
729
727
|
* plugin can't hide a missing policy. Throws a `ConformanceError` naming each
|
|
730
728
|
* table and command that leaks; removes every row it created.
|
|
731
729
|
*
|
|
732
730
|
* ```ts
|
|
733
|
-
* await expectTenantIsolation(
|
|
731
|
+
* await expectTenantIsolation(betterSupabase, {
|
|
734
732
|
* tenants: [
|
|
735
733
|
* { id: ACME, claims: { sub: alice, tenant_id: ACME } },
|
|
736
734
|
* { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
|
|
@@ -744,9 +742,9 @@ function env(names) {
|
|
|
744
742
|
* });
|
|
745
743
|
* ```
|
|
746
744
|
*/
|
|
747
|
-
async function expectTenantIsolation(
|
|
745
|
+
async function expectTenantIsolation(betterSupabase, options) {
|
|
748
746
|
const stack = options.stack ?? {};
|
|
749
|
-
const bare = new BetterSupabase(
|
|
747
|
+
const bare = new BetterSupabase(betterSupabase.schema);
|
|
750
748
|
const url = stack.url ?? env(ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
|
|
751
749
|
const secretKey = stack.secretKey ?? env(ENV_VARIABLES.secretKey);
|
|
752
750
|
if (!secretKey) throw new TypeError("expectTenantIsolation needs stack.secretKey or $SUPABASE_SECRET_KEY to seed rows.");
|
|
@@ -816,4 +814,105 @@ async function expectTenantIsolation(sb, options) {
|
|
|
816
814
|
}
|
|
817
815
|
}
|
|
818
816
|
//#endregion
|
|
817
|
+
//#region src/testing/seed.ts
|
|
818
|
+
const SEED = Symbol.for("better-supabase.seed");
|
|
819
|
+
function isSeed(value) {
|
|
820
|
+
return typeof value === "object" && value !== null && SEED in value;
|
|
821
|
+
}
|
|
822
|
+
const quote = (text) => `'${text.replaceAll("'", "''")}'`;
|
|
823
|
+
function arrayLiteral(values) {
|
|
824
|
+
return `{${values.map((value) => {
|
|
825
|
+
if (value === null) return "NULL";
|
|
826
|
+
if (Array.isArray(value)) return arrayLiteral(value);
|
|
827
|
+
return `"${(temporalText(value) ?? String(value)).replaceAll(/[\\"]/g, "\\$&")}"`;
|
|
828
|
+
}).join(",")}}`;
|
|
829
|
+
}
|
|
830
|
+
function literal(value, column, where) {
|
|
831
|
+
if (value === null) return "null";
|
|
832
|
+
if (column.json) return quote(JSON.stringify(value));
|
|
833
|
+
if (Array.isArray(value)) return quote(arrayLiteral(value));
|
|
834
|
+
const temporalValue = temporalText(value);
|
|
835
|
+
if (temporalValue !== void 0) return quote(temporalValue);
|
|
836
|
+
const kind = typeof value;
|
|
837
|
+
switch (kind) {
|
|
838
|
+
case "string": return quote(value);
|
|
839
|
+
case "boolean":
|
|
840
|
+
case "bigint": return String(value);
|
|
841
|
+
case "number":
|
|
842
|
+
if (!Number.isFinite(value)) throw new TypeError(`${where}: ${String(value)} is not a finite number`);
|
|
843
|
+
return String(value);
|
|
844
|
+
case "object":
|
|
845
|
+
case "function":
|
|
846
|
+
case "symbol":
|
|
847
|
+
case "undefined": throw new TypeError(`${where}: unsupported value of type ${kind}`);
|
|
848
|
+
default: throw new TypeError(`${where}: unsupported value of type ${String(kind)}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const ident = (name) => `"${name.replaceAll("\"", "\"\"")}"`;
|
|
852
|
+
/** Parents first (by forward relations), otherwise in the order given. */
|
|
853
|
+
function order(meta, keys) {
|
|
854
|
+
const sorted = [];
|
|
855
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
856
|
+
const visit = (key) => {
|
|
857
|
+
if (sorted.includes(key) || visiting.has(key)) return;
|
|
858
|
+
visiting.add(key);
|
|
859
|
+
for (const relation of Object.values(meta.tables[key]?.relations ?? {})) if (relation.direction === "forward" && relation.table !== key && keys.includes(relation.table)) visit(relation.table);
|
|
860
|
+
visiting.delete(key);
|
|
861
|
+
sorted.push(key);
|
|
862
|
+
};
|
|
863
|
+
for (const key of keys) visit(key);
|
|
864
|
+
return sorted;
|
|
865
|
+
}
|
|
866
|
+
function statement(table, rows) {
|
|
867
|
+
const entries = Object.entries(rows);
|
|
868
|
+
if (entries.length === 0) return void 0;
|
|
869
|
+
const columns = [];
|
|
870
|
+
for (const [, row] of entries) for (const [key, value] of Object.entries(row)) {
|
|
871
|
+
if (value === void 0 || columns.includes(key)) continue;
|
|
872
|
+
const column = table.columns[key];
|
|
873
|
+
if (!column) throw new TypeError(`seed ${table.key}: unknown column "${key}"`);
|
|
874
|
+
if (column.generated) throw new TypeError(`seed ${table.key}: "${key}" is a generated column`);
|
|
875
|
+
columns.push(key);
|
|
876
|
+
}
|
|
877
|
+
const values = entries.map(([name, row]) => {
|
|
878
|
+
return ` (${columns.map((key) => row[key] === void 0 ? "default" : literal(row[key], table.columns[key], `seed ${table.key}.${name}.${key}`)).join(", ")})`;
|
|
879
|
+
});
|
|
880
|
+
return `insert into ${`${ident(table.schema)}.${ident(table.name)}`} (${columns.map((key) => ident(table.columns[key].db)).join(", ")}) values\n${values.join(",\n")}\non conflict do nothing;`;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Typed fixtures shared by `supabase/seeds` and your tests. Rows use app
|
|
884
|
+
* casing and Insert types; `better-supabase seed` renders them to SQL.
|
|
885
|
+
*
|
|
886
|
+
* ```ts
|
|
887
|
+
* export const seed = defineSeed(betterSupabase, {
|
|
888
|
+
* organizations: { acme: { id: ACME, name: 'Acme' } },
|
|
889
|
+
* customers: { first: { organizationId: ACME, name: 'First customer' } },
|
|
890
|
+
* });
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
function defineSeed(betterSupabase, fixtures) {
|
|
894
|
+
const meta = betterSupabase.meta;
|
|
895
|
+
const statements = () => {
|
|
896
|
+
const keys = Object.keys(fixtures).filter((key) => fixtures[key] !== void 0);
|
|
897
|
+
for (const key of keys) {
|
|
898
|
+
const table = meta.tables[key];
|
|
899
|
+
if (!table) throw new TypeError(`seed: unknown table "${key}"`);
|
|
900
|
+
if (table.kind === "view") throw new TypeError(`seed: "${key}" is a view`);
|
|
901
|
+
}
|
|
902
|
+
return order(meta, keys).flatMap((key) => {
|
|
903
|
+
const sql = statement(meta.tables[key], fixtures[key]);
|
|
904
|
+
return sql ? [sql] : [];
|
|
905
|
+
});
|
|
906
|
+
};
|
|
907
|
+
return {
|
|
908
|
+
[SEED]: true,
|
|
909
|
+
rows: fixtures,
|
|
910
|
+
statements,
|
|
911
|
+
sql: () => statements().join("\n\n"),
|
|
912
|
+
async insert(sql) {
|
|
913
|
+
for (const text of statements()) await sql.queryRaw(text);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
819
918
|
export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testPlugin };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference lib="esnext.temporal" />
|
|
1
2
|
//#region src/schema/types.d.ts
|
|
2
3
|
type Casing = "snake" | "camel";
|
|
3
4
|
/** Runtime metadata for one column. Keyed by the app (cased) column name. */
|
|
@@ -27,10 +28,12 @@ interface ColumnMeta {
|
|
|
27
28
|
readonly storage?: string;
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
|
-
* Runtime representation for a column: `
|
|
31
|
-
* `
|
|
31
|
+
* Runtime representation for a column: `instant` decodes `timestamptz` to
|
|
32
|
+
* `Temporal.Instant`, `plainDateTime` decodes `timestamp` to
|
|
33
|
+
* `Temporal.PlainDateTime`, and `bigint` and `string` read the exact text of
|
|
34
|
+
* `int8`/`numeric` values.
|
|
32
35
|
*/
|
|
33
|
-
type Codec = "
|
|
36
|
+
type Codec = "instant" | "plainDateTime" | "bigint" | "string";
|
|
34
37
|
interface RelationMeta {
|
|
35
38
|
/** App key of the target table. */
|
|
36
39
|
readonly table: string;
|
|
@@ -165,19 +168,19 @@ interface BucketMeta {
|
|
|
165
168
|
readonly fileSizeLimit?: string;
|
|
166
169
|
readonly allowedMimeTypes?: readonly string[];
|
|
167
170
|
}
|
|
168
|
-
interface
|
|
171
|
+
interface RelationTypes {
|
|
169
172
|
readonly table: string;
|
|
170
173
|
readonly kind: "one" | "many";
|
|
171
174
|
readonly nullable: boolean;
|
|
172
175
|
}
|
|
173
|
-
interface
|
|
176
|
+
interface ModelFlags {
|
|
174
177
|
readonly softDelete?: string;
|
|
175
178
|
readonly timestamps?: true;
|
|
176
179
|
readonly tenant?: string;
|
|
177
180
|
readonly actor?: true;
|
|
178
181
|
readonly version?: string;
|
|
179
182
|
}
|
|
180
|
-
interface
|
|
183
|
+
interface ModelTypes {
|
|
181
184
|
readonly Row: object;
|
|
182
185
|
readonly Insert: object;
|
|
183
186
|
readonly Update: object;
|
|
@@ -188,17 +191,17 @@ interface ModelShape {
|
|
|
188
191
|
readonly Checks?: string;
|
|
189
192
|
/** Foreign key constraint names on this table. */
|
|
190
193
|
readonly ForeignKeys?: string;
|
|
191
|
-
readonly Flags:
|
|
194
|
+
readonly Flags: ModelFlags;
|
|
192
195
|
}
|
|
193
196
|
type AnyModels = {
|
|
194
|
-
readonly [table: string]:
|
|
197
|
+
readonly [table: string]: ModelTypes;
|
|
195
198
|
};
|
|
196
|
-
type
|
|
199
|
+
type FunctionTypes = {
|
|
197
200
|
readonly Args: object;
|
|
198
201
|
readonly Returns: unknown;
|
|
199
202
|
};
|
|
200
203
|
type AnyFunctions = {
|
|
201
|
-
readonly [name: string]:
|
|
204
|
+
readonly [name: string]: FunctionTypes;
|
|
202
205
|
};
|
|
203
206
|
/**
|
|
204
207
|
* Carries the model types next to the runtime metadata. Created by
|
|
@@ -245,4 +248,4 @@ type IsUnion<T, U = T> = T extends unknown ? [U] extends [T] ? false : true : ne
|
|
|
245
248
|
type PrimaryKeyValue<M extends AnyModels, T extends keyof M> = true extends IsUnion<PrimaryKeyColumn<M, T>> ? { readonly [K in PrimaryKeyColumn<M, T>]: Row<M, T>[K]; } : Row<M, T>[PrimaryKeyColumn<M, T>];
|
|
246
249
|
type Simplify<T> = { [K in keyof T]: T[K]; } & {};
|
|
247
250
|
//#endregion
|
|
248
|
-
export { SchemaMeta as A, PrimaryKeyValue as C, Relations as D,
|
|
251
|
+
export { SchemaMeta as A, PrimaryKeyValue as C, Relations as D, RelationTypes as E, UniqueConstraint as F, UniqueKeyName as I, UniqueWhere as L, TableFlags as M, TableKey as N, Row as O, TableMeta as P, Update as R, PrimaryKeyColumn as S, RelationMeta as T, ModelFlags as _, Casing as a, PermdockBucketPolicy as b, Codec as c, Flags as d, ForeignKeyConstraint as f, Insert as g, FunctionsOf as h, BucketPolicyName as i, Simplify as j, Schema as k, ColumnMeta as l, FunctionTypes as m, AnyModels as n, CheckConstraint as o, FunctionMeta as p, BucketMeta as r, ClaimsMeta as s, AnyFunctions as t, DatabaseOf as u, ModelTypes as v, RealtimeTableMeta as w, PermdockTopicPolicy as x, ModelsOf as y };
|