better-supabase 0.1.0 → 0.3.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 +34 -31
- package/dist/base64-CQeB1eSf.js +42 -0
- package/dist/claims-CoTobNC-.js +25 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +4 -14
- package/dist/{compiler-DL18ukPe.d.ts → compiler-2zqGzxVZ.d.ts} +2 -2
- package/dist/config/index.d.ts +4 -3
- package/dist/config/index.js +5 -3
- package/dist/config-Be4GwsqI.js +129 -0
- package/dist/{index-BD6wKthp.d.ts → config-C1CYcm6o.d.ts} +75 -54
- package/dist/{config-D-aR2ZoN.js → config-DOsGYSiT.js} +14 -136
- package/dist/{define-QGkq04oe.js → define-DF341GDj.js} +133 -110
- package/dist/{define-CTfWw-SS.d.ts → define-DUbydmJa.d.ts} +32 -14
- package/dist/edge/index.d.ts +4 -4
- package/dist/edge/index.js +4 -4
- package/dist/entitlements-CTEBm7IB.js +20 -0
- package/dist/entitlements-DvJXoI02.d.ts +42 -0
- package/dist/env/index.js +203 -1
- package/dist/events/index.d.ts +1 -1
- package/dist/events/index.js +139 -1
- package/dist/{events-2OJsVG_d.d.ts → events-Do8jXh4D.d.ts} +3 -3
- package/dist/{executor-Bje4suQF.d.ts → executor-B26lgbHD.d.ts} +2 -2
- package/dist/{executor-Ceq60hPM.js → executor-C57jx2pn.js} +7 -2
- package/dist/{executor-Bazhgsgq.d.ts → executor-DynmIWhq.d.ts} +2 -2
- package/dist/guards-BnzDvFrz.js +10 -0
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +4 -4
- package/dist/{index-Hf8PP9sP.d.ts → index-BNsABq7P.d.ts} +17 -13
- package/dist/{index-JXHBuS53.d.ts → index-CADlNHK-.d.ts} +27 -14
- package/dist/{index-Nxo3TJcm.d.ts → index-CTTMS6_1.d.ts} +27 -8
- package/dist/index-Cf-D2jk1.d.ts +50 -0
- package/dist/{index-Dup-nSe9.d.ts → index-CriFFJ_p.d.ts} +4 -4
- package/dist/{index-DQVvHzjW.d.ts → index-GV77NpRP.d.ts} +5 -4
- package/dist/{index-Sw43J8HG.d.ts → index-UJ-LUxtI.d.ts} +6 -6
- package/dist/{index-cEeweTeM.d.ts → index-kpgopLNv.d.ts} +4 -3
- package/dist/{index-DRid0qIv.d.ts → index-qriRM9cZ.d.ts} +7 -2
- package/dist/index.d.ts +13 -13
- package/dist/index.js +18 -8
- package/dist/jobs/index.d.ts +25 -24
- package/dist/jobs/index.js +19 -12
- package/dist/{json-schema-BbwO2mZg.js → json-schema--aOXAGfU.js} +9 -3
- package/dist/lint/index.js +1 -1
- package/dist/list/index.d.ts +2 -2
- package/dist/list/index.js +128 -93
- package/dist/{live-6b9i72Ux.d.ts → live-QE3UPbGm.d.ts} +27 -6
- package/dist/logger-glM28NVN.js +21 -0
- package/dist/mcp/index.d.ts +59 -15
- package/dist/mcp/index.js +229 -67
- package/dist/{mfa-CpogP66z.js → mfa-BxoR1RkN.js} +97 -22
- package/dist/{mfa-TVia554c.d.ts → mfa-x3V3vGh-.d.ts} +1 -1
- package/dist/next/index.d.ts +51 -38
- package/dist/next/index.js +31 -25
- package/dist/openapi/index.d.ts +1 -1
- package/dist/openapi/index.js +58 -35
- package/dist/orpc/index.d.ts +6 -6
- package/dist/orpc/index.js +3 -3
- package/dist/otel/index.d.ts +3 -3
- package/dist/otel/index.js +5 -3
- package/dist/{template-BeOGnynM.js → permdock-sql-Bta_G0v-.js} +32 -3
- package/dist/permdock-sql-DImJULY-.d.ts +17 -0
- package/dist/{plugin-HvGqCurB.d.ts → plugin-DVRB8qNE.d.ts} +5 -4
- package/dist/plugins/actor/index.d.ts +1 -1
- package/dist/plugins/rules/index.d.ts +4 -3
- package/dist/plugins/rules/index.js +8 -15
- package/dist/plugins/soft-delete/index.d.ts +3 -3
- package/dist/plugins/soft-delete/index.js +2 -2
- package/dist/plugins/tenant/index.d.ts +10 -4
- package/dist/plugins/tenant/index.js +15 -13
- package/dist/plugins/timestamps/index.d.ts +1 -1
- package/dist/plugins/timestamps/index.js +1 -1
- package/dist/plugins/validation/index.d.ts +2 -2
- package/dist/pool-B9qsLyFR.d.ts +71 -0
- package/dist/postgres/index.d.ts +10 -5
- package/dist/postgres/index.js +36 -16
- package/dist/{postgrest-Dm-vhY6R.d.ts → postgrest-DmBlevh-.d.ts} +1 -1
- package/dist/{problem-DPu6Fh-t.js → problem-CGt1ADix.js} +14 -8
- package/dist/{problem-DlgB0lYl.d.ts → problem-Z2RTQjZD.d.ts} +6 -4
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +1 -1
- package/dist/{query-ChvDbsDE.js → query-BdKLTPlj.js} +2 -2
- package/dist/react/index.d.ts +7 -7
- package/dist/react/index.js +3 -3
- package/dist/react/server.d.ts +5 -5
- package/dist/react/server.js +1 -1
- package/dist/react/session.d.ts +21 -2
- package/dist/react/session.js +19 -1
- package/dist/{react-CrOvrnIP.js → react-DNBYNazu.js} +28 -23
- package/dist/{read-set-DhX2d04c.js → read-set-D0qBcboM.js} +3 -3
- package/dist/realtime/index.d.ts +4 -2
- package/dist/realtime/index.js +30 -14
- package/dist/{resolve-DW8h9NPm.d.ts → resolve-BRciX2jK.d.ts} +25 -9
- package/dist/{resource-SkPTRJDD.d.ts → resource-CwVgiI5q.d.ts} +16 -7
- package/dist/{resource-CYiu6Ygi.js → resource-D30rp0GN.js} +38 -16
- package/dist/{respond-4-dhoV7x.js → respond-C44EZAJo.js} +38 -19
- package/dist/{respond-PoTpy5DT.d.ts → respond-DAkPIdTH.d.ts} +23 -17
- package/dist/{result-CTkCZ6JA.d.ts → result-c2OYgCV2.d.ts} +13 -8
- package/dist/server/index.d.ts +35 -10
- package/dist/server/index.js +38 -9
- package/dist/{simplify-oHiDKRVQ.js → simplify-LakqK1vH.js} +22 -28
- package/dist/{spec-pins-t6sPaHvl.js → spec-pins-C3oc-Tdp.js} +1 -1
- package/dist/{spec-pins-CJPVghHy.d.ts → spec-pins-CqorWDnO.d.ts} +1 -1
- package/dist/sql/index.d.ts +139 -0
- package/dist/sql/index.js +1983 -0
- package/dist/{sql-M8l3fnS0.js → sql-BIgk12ew.js} +72 -16
- package/dist/ssr/index.d.ts +5 -5
- package/dist/ssr/index.js +3 -3
- package/dist/{standard-CyhRzctb.d.ts → standard-eudBiRk1.d.ts} +1 -1
- package/dist/{stats-3iy8u3_P.js → stats-D78UQ8Yu.js} +8 -7
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.js +530 -1
- package/dist/{tables-rZxRxNPW.js → tables-Dhha3BXM.js} +5 -5
- package/dist/temporal-BwshQk0W.js +27 -0
- package/dist/temporal-required-CMxeSVPo.js +19 -0
- package/dist/testing/index.d.ts +49 -16
- package/dist/testing/index.js +233 -66
- package/dist/{types-C2fslP1z.d.ts → types-DONv9SHN.d.ts} +68 -12
- package/dist/{version-Cqq1m3UY.js → version-BfpG2gks.js} +1 -1
- package/dist/view-3Po_PVlr.d.ts +79 -0
- package/dist/{view-B6F9QbG-.js → view-wZ-9qIUz.js} +12 -2
- package/dist/webhooks/index.d.ts +19 -17
- package/dist/webhooks/index.js +1 -1
- package/dist/{webhooks-DbMTJJX2.js → webhooks-CAIFQSTM.js} +12 -10
- package/package.json +30 -30
- package/schemas/config-v1.json +80 -5
- package/schemas/doctor-report-v1.json +18 -1
- package/schemas/snapshot-v2.json +39 -0
- package/skills/better-supabase/SKILL.md +23 -11
- package/skills/better-supabase/references/plugins.md +28 -16
- package/skills/better-supabase/references/troubleshooting.md +15 -15
- package/skills/better-supabase-api/SKILL.md +9 -9
- package/skills/better-supabase-api/references/adapters.md +30 -25
- package/skills/better-supabase-testing/SKILL.md +15 -11
- package/bin/better-supabase.js +0 -3
- package/dist/casing-da0uRTqt.js +0 -14
- package/dist/cli/bin.d.ts +0 -1
- package/dist/cli/bin.js +0 -15
- package/dist/cli/index.d.ts +0 -226
- package/dist/cli/index.js +0 -3
- package/dist/cli-CoByWYja.js +0 -6472
- package/dist/entitlements-BBg61DQZ.d.ts +0 -20
- package/dist/entitlements-BC_khwRy.js +0 -17
- package/dist/env-BEToab0I.js +0 -204
- package/dist/events-BuzTgn2D.js +0 -135
- package/dist/pool-BFeqzwQS.d.ts +0 -37
- package/dist/seed-CzecODcJ.js +0 -101
- package/dist/session-C3osL21k.d.ts +0 -21
- package/dist/session-CD-3orO_.js +0 -19
- package/dist/storage-BWXVUXGk.js +0 -461
- package/dist/view-B0ltUmTi.d.ts +0 -43
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import { t as
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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-DynmIWhq.js";
|
|
3
|
+
import { P as DbStats, l as CacheAdapter, t as BetterSupabase, y as Db } from "../define-DUbydmJa.js";
|
|
4
|
+
import { h as RequestContext, n as AnyPlugin } from "../plugin-DVRB8qNE.js";
|
|
5
|
+
import { b as ResolvedConfig, d as Generator, f as GeneratorInput } from "../config-C1CYcm6o.js";
|
|
6
|
+
import "../index-Cf-D2jk1.js";
|
|
7
|
+
import { t as SqlClient } from "../executor-B26lgbHD.js";
|
|
8
|
+
import { r as Postgres } from "../pool-B9qsLyFR.js";
|
|
9
|
+
import { n as AuthResolver } from "../resolve-BRciX2jK.js";
|
|
10
|
+
import { r as EventSink, t as CloudEvent } from "../index-GV77NpRP.js";
|
|
10
11
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
12
|
+
import { webcrypto } from "node:crypto";
|
|
11
13
|
//#region src/testing/jwt.d.ts
|
|
12
14
|
interface TestJwtClaims {
|
|
13
15
|
readonly sub: string;
|
|
@@ -31,13 +33,32 @@ export declare function localAuth(secret: string): AuthResolver;
|
|
|
31
33
|
interface TestSigner {
|
|
32
34
|
/** Public JWKS; pass as `jwks` to `resolveAuth` / `createServer({ auth: { jwks } })`. */
|
|
33
35
|
readonly jwks: {
|
|
34
|
-
readonly keys: readonly JsonWebKey[];
|
|
36
|
+
readonly keys: readonly webcrypto.JsonWebKey[];
|
|
35
37
|
};
|
|
36
38
|
sign(claims: TestJwtClaims): Promise<string>;
|
|
37
39
|
}
|
|
40
|
+
/** A private ES256 JWK, as `better-supabase keys` writes to `supabase/signing_keys.json`. */
|
|
41
|
+
interface SigningJwk extends webcrypto.JsonWebKey {
|
|
42
|
+
readonly kid: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Signs an ES256 access token with a private JWK, the way the local stack
|
|
46
|
+
* signs them once `signing_keys_path` is set. Test-only.
|
|
47
|
+
*/
|
|
48
|
+
export declare function signTestJwtWithKey(key: SigningJwk, claims: TestJwtClaims): Promise<string>;
|
|
38
49
|
/** An in-memory ES256 key pair that signs tokens like Supabase Auth's asymmetric keys. */
|
|
39
50
|
export declare function createTestSigner(): Promise<TestSigner>;
|
|
40
51
|
//#endregion
|
|
52
|
+
//#region src/testing/local-key.d.ts
|
|
53
|
+
/**
|
|
54
|
+
* The key the local stack signs with: the first entry of the file
|
|
55
|
+
* `better-supabase keys` wrote. `path` defaults to `$SUPABASE_SIGNING_KEYS_PATH`,
|
|
56
|
+
* then `signing_keys_path` from the nearest `supabase/config.toml`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function localSigningKey(path?: string): Promise<SigningJwk>;
|
|
59
|
+
/** Signs a token with the local stack's ES256 key (see `localSigningKey`). Test-only. */
|
|
60
|
+
export declare function signLocalJwt(claims: TestJwtClaims, path?: string): Promise<string>;
|
|
61
|
+
//#endregion
|
|
41
62
|
//#region src/testing/as-user.d.ts
|
|
42
63
|
/** The local CLI stack's default JWT secret. */
|
|
43
64
|
export declare const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long";
|
|
@@ -46,7 +67,17 @@ interface LocalStack {
|
|
|
46
67
|
readonly url?: string;
|
|
47
68
|
/** Defaults to `$SUPABASE_PUBLISHABLE_KEY`, then `$SUPABASE_ANON_KEY`. */
|
|
48
69
|
readonly publishableKey?: string;
|
|
49
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* `ES256` (default) signs with the key `better-supabase keys` wrote, as
|
|
72
|
+
* the hosted project's asymmetric keys do. `HS256` signs with the shared
|
|
73
|
+
* JWT secret and only works against a local stack.
|
|
74
|
+
*/
|
|
75
|
+
readonly alg?: "ES256" | "HS256";
|
|
76
|
+
/** The private ES256 key. Defaults to the first key in `signingKeysPath`. */
|
|
77
|
+
readonly signingKey?: SigningJwk;
|
|
78
|
+
/** Defaults to `$SUPABASE_SIGNING_KEYS_PATH`, then `signing_keys_path` in `supabase/config.toml`. */
|
|
79
|
+
readonly signingKeysPath?: string;
|
|
80
|
+
/** For `alg: 'HS256'`. Defaults to `$SUPABASE_JWT_SECRET`, then the CLI default. */
|
|
50
81
|
readonly jwtSecret?: string;
|
|
51
82
|
/** Service-role key for `expectTenantIsolation`; defaults to `$SUPABASE_SECRET_KEY`. */
|
|
52
83
|
readonly secretKey?: string;
|
|
@@ -65,10 +96,12 @@ interface TestUser<M extends AnyModels, F extends AnyFunctions, E> {
|
|
|
65
96
|
}
|
|
66
97
|
/**
|
|
67
98
|
* A test user against the local stack: a signed token and repositories that
|
|
68
|
-
* run as that user, for live RLS tests.
|
|
99
|
+
* run as that user, for live RLS tests. The token is ES256-signed with the
|
|
100
|
+
* key from `better-supabase keys`; pass `{ alg: 'HS256' }` for a stack that
|
|
101
|
+
* still uses the shared JWT secret.
|
|
69
102
|
*
|
|
70
103
|
* ```ts
|
|
71
|
-
* const alice = await asUser(sb, { sub: aliceId,
|
|
104
|
+
* const alice = await asUser(sb, { sub: aliceId, tenant_id: acme });
|
|
72
105
|
* expect(await alice.db.customers.count().orThrow()).toBe(3);
|
|
73
106
|
* ```
|
|
74
107
|
*/
|
|
@@ -247,8 +280,8 @@ interface TenantIsolationOptions<M extends AnyModels> {
|
|
|
247
280
|
* ```ts
|
|
248
281
|
* await expectTenantIsolation(sb, {
|
|
249
282
|
* tenants: [
|
|
250
|
-
* { id: ACME, claims: { sub: alice,
|
|
251
|
-
* { id: GLOBEX, claims: { sub: bob,
|
|
283
|
+
* { id: ACME, claims: { sub: alice, tenant_id: ACME } },
|
|
284
|
+
* { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
|
|
252
285
|
* ],
|
|
253
286
|
* tables: {
|
|
254
287
|
* tags: {
|
|
@@ -291,4 +324,4 @@ export declare function isSeed(value: unknown): value is Seed<unknown>;
|
|
|
291
324
|
*/
|
|
292
325
|
export declare function defineSeed<M extends AnyModels, D, F extends AnyFunctions, E, const S extends SeedFixtures<M>>(sb: BetterSupabase<M, D, F, E>, fixtures: S & ExactSeed<M, S>): Seed<S>;
|
|
293
326
|
//#endregion
|
|
294
|
-
export type { BudgetPage, BudgetResponse, ConformanceCheck, ConformanceReport, DbBudgetExpectation, ExactSeed, IsolationTable, IsolationTables, IsolationTenant, LocalStack, MeasuredRender, Seed, SeedFixtures, TenantIsolationOptions, TestAuthResolverOptions, TestEventSinkOptions, TestExecutorOptions, TestGeneratorOptions, TestJwtClaims, TestPluginOptions, TestSigner, TestUser };
|
|
327
|
+
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,20 +1,20 @@
|
|
|
1
1
|
import { n as dbError } from "../errors-Bl3rkOzo.js";
|
|
2
|
+
import { i as temporalText } from "../temporal-BwshQk0W.js";
|
|
3
|
+
import { t as temporal } from "../temporal-required-CMxeSVPo.js";
|
|
2
4
|
import { i as ok } from "../result-DWXatkd6.js";
|
|
3
|
-
import { t as BetterSupabase } from "../define-
|
|
4
|
-
import { r as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { n as
|
|
5
|
+
import { t as BetterSupabase } from "../define-DF341GDj.js";
|
|
6
|
+
import { a as toBase64Url, r as fromBase64, t as base64ToText } from "../base64-CQeB1eSf.js";
|
|
7
|
+
import { r as resolveConfig } from "../config-Be4GwsqI.js";
|
|
8
|
+
import { t as isList } from "../guards-BnzDvFrz.js";
|
|
9
|
+
import { ENV_VARIABLES } from "../env/index.js";
|
|
10
|
+
import { n as postgresExecutor } from "../executor-C57jx2pn.js";
|
|
11
|
+
import { toCloudEvents } from "../events/index.js";
|
|
9
12
|
import { createClient } from "@supabase/supabase-js";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
16
|
//#region src/testing/jwt.ts
|
|
11
17
|
const encoder = new TextEncoder();
|
|
12
|
-
function base64url(input) {
|
|
13
|
-
const bytes = typeof input === "string" ? encoder.encode(input) : input;
|
|
14
|
-
let binary = "";
|
|
15
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
16
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
17
|
-
}
|
|
18
18
|
/**
|
|
19
19
|
* Signs an HS256 access token the local Supabase stack accepts. Test-only:
|
|
20
20
|
* production tokens come from Supabase Auth.
|
|
@@ -30,21 +30,17 @@ async function signTestJwt(secret, claims) {
|
|
|
30
30
|
exp: now + expiresIn,
|
|
31
31
|
...rest
|
|
32
32
|
};
|
|
33
|
-
const header =
|
|
33
|
+
const header = toBase64Url(JSON.stringify({
|
|
34
34
|
alg: "HS256",
|
|
35
35
|
typ: "JWT"
|
|
36
36
|
}));
|
|
37
|
-
const body =
|
|
37
|
+
const body = toBase64Url(JSON.stringify(payload));
|
|
38
38
|
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
39
39
|
name: "HMAC",
|
|
40
40
|
hash: "SHA-256"
|
|
41
41
|
}, false, ["sign"]);
|
|
42
42
|
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${header}.${body}`));
|
|
43
|
-
return `${header}.${body}.${
|
|
44
|
-
}
|
|
45
|
-
function base64urlDecode(input) {
|
|
46
|
-
const binary = atob(input.replaceAll("-", "+").replaceAll("_", "/"));
|
|
47
|
-
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
43
|
+
return `${header}.${body}.${toBase64Url(new Uint8Array(signature))}`;
|
|
48
44
|
}
|
|
49
45
|
/**
|
|
50
46
|
* Accepts `signTestJwt` tokens as users, for API servers tested against the
|
|
@@ -61,9 +57,9 @@ function localAuth(secret) {
|
|
|
61
57
|
async resolve(request) {
|
|
62
58
|
const token = /^Bearer (.+)$/i.exec(request.headers.get("authorization") ?? "")?.[1];
|
|
63
59
|
const [header, body, signature] = token?.split(".") ?? [];
|
|
64
|
-
if (!token || !header || !body || !signature) return
|
|
65
|
-
if (!await crypto.subtle.verify("HMAC", await key,
|
|
66
|
-
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));
|
|
67
63
|
if (typeof claims.exp === "number" && claims.exp * 1e3 < Date.now()) return {
|
|
68
64
|
kind: "invalid",
|
|
69
65
|
error: dbError("unauthorized", "The token has expired")
|
|
@@ -83,6 +79,40 @@ function localAuth(secret) {
|
|
|
83
79
|
}
|
|
84
80
|
};
|
|
85
81
|
}
|
|
82
|
+
async function signEs256(key, kid, claims) {
|
|
83
|
+
const { expiresIn = 3600, ...rest } = claims;
|
|
84
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
85
|
+
const payload = {
|
|
86
|
+
aud: "authenticated",
|
|
87
|
+
role: "authenticated",
|
|
88
|
+
aal: "aal1",
|
|
89
|
+
iat: now,
|
|
90
|
+
exp: now + expiresIn,
|
|
91
|
+
...rest
|
|
92
|
+
};
|
|
93
|
+
const header = toBase64Url(JSON.stringify({
|
|
94
|
+
alg: "ES256",
|
|
95
|
+
typ: "JWT",
|
|
96
|
+
kid
|
|
97
|
+
}));
|
|
98
|
+
const body = toBase64Url(JSON.stringify(payload));
|
|
99
|
+
const signature = await crypto.subtle.sign({
|
|
100
|
+
name: "ECDSA",
|
|
101
|
+
hash: "SHA-256"
|
|
102
|
+
}, key, encoder.encode(`${header}.${body}`));
|
|
103
|
+
return `${header}.${body}.${toBase64Url(new Uint8Array(signature))}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Signs an ES256 access token with a private JWK, the way the local stack
|
|
107
|
+
* signs them once `signing_keys_path` is set. Test-only.
|
|
108
|
+
*/
|
|
109
|
+
async function signTestJwtWithKey(key, claims) {
|
|
110
|
+
const { kid, ...jwk } = key;
|
|
111
|
+
return signEs256(await crypto.subtle.importKey("jwk", jwk, {
|
|
112
|
+
name: "ECDSA",
|
|
113
|
+
namedCurve: "P-256"
|
|
114
|
+
}, false, ["sign"]), kid, claims);
|
|
115
|
+
}
|
|
86
116
|
/** An in-memory ES256 key pair that signs tokens like Supabase Auth's asymmetric keys. */
|
|
87
117
|
async function createTestSigner() {
|
|
88
118
|
const pair = await crypto.subtle.generateKey({
|
|
@@ -97,32 +127,44 @@ async function createTestSigner() {
|
|
|
97
127
|
alg: "ES256",
|
|
98
128
|
use: "sig"
|
|
99
129
|
}] },
|
|
100
|
-
|
|
101
|
-
const { expiresIn = 3600, ...rest } = claims;
|
|
102
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
103
|
-
const payload = {
|
|
104
|
-
aud: "authenticated",
|
|
105
|
-
role: "authenticated",
|
|
106
|
-
aal: "aal1",
|
|
107
|
-
iat: now,
|
|
108
|
-
exp: now + expiresIn,
|
|
109
|
-
...rest
|
|
110
|
-
};
|
|
111
|
-
const header = base64url(JSON.stringify({
|
|
112
|
-
alg: "ES256",
|
|
113
|
-
typ: "JWT",
|
|
114
|
-
kid
|
|
115
|
-
}));
|
|
116
|
-
const body = base64url(JSON.stringify(payload));
|
|
117
|
-
const signature = await crypto.subtle.sign({
|
|
118
|
-
name: "ECDSA",
|
|
119
|
-
hash: "SHA-256"
|
|
120
|
-
}, pair.privateKey, encoder.encode(`${header}.${body}`));
|
|
121
|
-
return `${header}.${body}.${base64url(new Uint8Array(signature))}`;
|
|
122
|
-
}
|
|
130
|
+
sign: (claims) => signEs256(pair.privateKey, kid, claims)
|
|
123
131
|
};
|
|
124
132
|
}
|
|
125
133
|
//#endregion
|
|
134
|
+
//#region src/testing/local-key.ts
|
|
135
|
+
const SIGNING_KEYS_PATH = /^\s*signing_keys_path\s*=\s*"([^"]+)"/m;
|
|
136
|
+
/** `supabase/config.toml`'s `signing_keys_path`, else `supabase/signing_keys.json`, in `from` or a parent. */
|
|
137
|
+
async function findSigningKeys(from) {
|
|
138
|
+
for (let dir = resolve(from);; dir = dirname(dir)) {
|
|
139
|
+
const supabase = join(dir, "supabase");
|
|
140
|
+
const toml = join(supabase, "config.toml");
|
|
141
|
+
if (existsSync(toml)) {
|
|
142
|
+
const configured = SIGNING_KEYS_PATH.exec(await readFile(toml, "utf8"))?.[1];
|
|
143
|
+
const path = resolve(supabase, configured ?? "signing_keys.json");
|
|
144
|
+
if (existsSync(path)) return path;
|
|
145
|
+
}
|
|
146
|
+
if (dirname(dir) === dir) return void 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const isSigningKey = (value) => typeof value === "object" && value !== null && "kid" in value && typeof value.kid === "string" && "d" in value && typeof value.d === "string";
|
|
150
|
+
/**
|
|
151
|
+
* The key the local stack signs with: the first entry of the file
|
|
152
|
+
* `better-supabase keys` wrote. `path` defaults to `$SUPABASE_SIGNING_KEYS_PATH`,
|
|
153
|
+
* then `signing_keys_path` from the nearest `supabase/config.toml`.
|
|
154
|
+
*/
|
|
155
|
+
async function localSigningKey(path) {
|
|
156
|
+
const file = path ?? process.env["SUPABASE_SIGNING_KEYS_PATH"] ?? await findSigningKeys(process.cwd());
|
|
157
|
+
if (!file || !existsSync(file)) throw new TypeError("No local signing key. Run `better-supabase keys`, set `signing_keys_path` in supabase/config.toml and restart the stack, or pass { alg: \"HS256\" } to sign with the local JWT secret.");
|
|
158
|
+
const keys = JSON.parse(await readFile(file, "utf8"));
|
|
159
|
+
const first = Array.isArray(keys) ? keys[0] : void 0;
|
|
160
|
+
if (!isSigningKey(first)) throw new TypeError(`${file} has no private ES256 key first.`);
|
|
161
|
+
return first;
|
|
162
|
+
}
|
|
163
|
+
/** Signs a token with the local stack's ES256 key (see `localSigningKey`). Test-only. */
|
|
164
|
+
async function signLocalJwt(claims, path) {
|
|
165
|
+
return signTestJwtWithKey(await localSigningKey(path), claims);
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
126
168
|
//#region src/testing/as-user.ts
|
|
127
169
|
/** The local CLI stack's default JWT secret. */
|
|
128
170
|
const LOCAL_JWT_SECRET = "super-secret-jwt-token-with-at-least-32-characters-long";
|
|
@@ -133,12 +175,30 @@ function env$1(...names) {
|
|
|
133
175
|
if (value) return value;
|
|
134
176
|
}
|
|
135
177
|
}
|
|
178
|
+
const LOCAL_HOSTS = /* @__PURE__ */ new Set([
|
|
179
|
+
"localhost",
|
|
180
|
+
"127.0.0.1",
|
|
181
|
+
"[::1]",
|
|
182
|
+
"host.docker.internal"
|
|
183
|
+
]);
|
|
184
|
+
async function signFor(stack, url, claims) {
|
|
185
|
+
const alg = stack.alg ?? "ES256";
|
|
186
|
+
switch (alg) {
|
|
187
|
+
case "ES256": return signTestJwtWithKey(stack.signingKey ?? await localSigningKey(stack.signingKeysPath), claims);
|
|
188
|
+
case "HS256":
|
|
189
|
+
if (!LOCAL_HOSTS.has(new URL(url).hostname)) throw new TypeError(`asUser signs HS256 tokens only for a local stack, not ${url}. Use the ES256 key from \`better-supabase keys\`.`);
|
|
190
|
+
return signTestJwt(stack.jwtSecret ?? env$1("SUPABASE_JWT_SECRET") ?? "super-secret-jwt-token-with-at-least-32-characters-long", claims);
|
|
191
|
+
default: throw new TypeError(`asUser: unknown alg "${String(alg)}"`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
136
194
|
/**
|
|
137
195
|
* A test user against the local stack: a signed token and repositories that
|
|
138
|
-
* run as that user, for live RLS tests.
|
|
196
|
+
* run as that user, for live RLS tests. The token is ES256-signed with the
|
|
197
|
+
* key from `better-supabase keys`; pass `{ alg: 'HS256' }` for a stack that
|
|
198
|
+
* still uses the shared JWT secret.
|
|
139
199
|
*
|
|
140
200
|
* ```ts
|
|
141
|
-
* const alice = await asUser(sb, { sub: aliceId,
|
|
201
|
+
* const alice = await asUser(sb, { sub: aliceId, tenant_id: acme });
|
|
142
202
|
* expect(await alice.db.customers.count().orThrow()).toBe(3);
|
|
143
203
|
* ```
|
|
144
204
|
*/
|
|
@@ -146,7 +206,7 @@ async function asUser(sb, claims, stack = {}) {
|
|
|
146
206
|
const url = stack.url ?? env$1(...ENV_VARIABLES.url) ?? "http://127.0.0.1:54321";
|
|
147
207
|
const publishableKey = stack.publishableKey ?? env$1(...ENV_VARIABLES.publishableKey, "SUPABASE_ANON_KEY");
|
|
148
208
|
if (!publishableKey) throw new TypeError("asUser needs publishableKey or $SUPABASE_PUBLISHABLE_KEY. Run `better-supabase env` to write it to .env.local.");
|
|
149
|
-
const token = await
|
|
209
|
+
const token = await signFor(stack, url, claims);
|
|
150
210
|
const full = {
|
|
151
211
|
role: "authenticated",
|
|
152
212
|
...claims
|
|
@@ -179,7 +239,9 @@ function settle(promise, ms) {
|
|
|
179
239
|
let timer;
|
|
180
240
|
return Promise.race([promise, new Promise((done) => {
|
|
181
241
|
timer = setTimeout(done, ms);
|
|
182
|
-
})]).finally(() =>
|
|
242
|
+
})]).finally(() => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
});
|
|
183
245
|
}
|
|
184
246
|
/**
|
|
185
247
|
* Measures every document and RSC navigation response during `during` and
|
|
@@ -299,7 +361,9 @@ function tableKey(sb, table) {
|
|
|
299
361
|
if (!key || !(key in sb.meta.tables)) throw new TypeError(`better-supabase: unknown table "${String(key)}"`);
|
|
300
362
|
return key;
|
|
301
363
|
}
|
|
302
|
-
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
|
+
}];
|
|
303
367
|
/**
|
|
304
368
|
* Proves an `Executor` meets the contract: rows keyed by the selection's
|
|
305
369
|
* aliases, counts, `aborted` errors, failures returned as `Result`s rather
|
|
@@ -313,7 +377,7 @@ function testExecutor(executor, options) {
|
|
|
313
377
|
const { sb } = options;
|
|
314
378
|
const table = tableKey(sb, options.table);
|
|
315
379
|
const context = { errorMappers: [] };
|
|
316
|
-
const read = () => captureOp(sb, (db) => db[table]
|
|
380
|
+
const read = () => captureOp(sb, (db) => db[table]["findMany"]({ limit: 2 }));
|
|
317
381
|
const create = options.create;
|
|
318
382
|
return conform(`Executor "${executor.name}"`, [
|
|
319
383
|
hasName(executor),
|
|
@@ -321,7 +385,7 @@ function testExecutor(executor, options) {
|
|
|
321
385
|
const op = await read();
|
|
322
386
|
const result = await executor.execute(op, context);
|
|
323
387
|
expect(result.ok, `select failed: ${result.ok ? "" : result.error.message}`);
|
|
324
|
-
expect(
|
|
388
|
+
expect(isList(result.data.rows), "rows must be an array");
|
|
325
389
|
expect(result.data.rows.length <= 2, `limit 2 returned ${result.data.rows.length} rows`);
|
|
326
390
|
expect(result.data.count === null || typeof result.data.count === "number", "count must be a number or null");
|
|
327
391
|
if (op.kind !== "select") return;
|
|
@@ -332,7 +396,7 @@ function testExecutor(executor, options) {
|
|
|
332
396
|
}
|
|
333
397
|
}],
|
|
334
398
|
["counts rows", async () => {
|
|
335
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
399
|
+
const op = await captureOp(sb, (db) => db[table]["count"]());
|
|
336
400
|
const result = await executor.execute(op, context);
|
|
337
401
|
expect(result.ok, `count failed: ${result.ok ? "" : result.error.message}`);
|
|
338
402
|
expect(typeof result.data.count === "number" && result.data.count >= 0, "count must be a number");
|
|
@@ -380,7 +444,7 @@ function testExecutor(executor, options) {
|
|
|
380
444
|
}],
|
|
381
445
|
executor.batch && ["batch returns one result per operation, in order", async () => {
|
|
382
446
|
const rows = await read();
|
|
383
|
-
const count = await captureOp(sb, (db) => db[table]
|
|
447
|
+
const count = await captureOp(sb, (db) => db[table]["count"]());
|
|
384
448
|
const missing = {
|
|
385
449
|
...rows,
|
|
386
450
|
table: {
|
|
@@ -401,15 +465,15 @@ function testExecutor(executor, options) {
|
|
|
401
465
|
}],
|
|
402
466
|
create && ["round-trips a write", async () => {
|
|
403
467
|
const repository = sb.connect(executor)[table];
|
|
404
|
-
const created = await repository
|
|
468
|
+
const created = await repository["create"](create);
|
|
405
469
|
expect(created.ok, `create failed: ${JSON.stringify(created.error)}`);
|
|
406
470
|
const row = created.data;
|
|
407
471
|
for (const [key, value] of Object.entries(create)) expect(same(row[key], value), `created row has ${key} = ${JSON.stringify(row[key])}`);
|
|
408
472
|
const [primary] = sb.meta.tables[table].primaryKey;
|
|
409
473
|
expect(primary, "write checks need a table with a primary key");
|
|
410
|
-
const removed = await repository
|
|
474
|
+
const removed = await repository["delete"](row[primary]);
|
|
411
475
|
expect(removed.ok, `delete failed: ${JSON.stringify(removed.error)}`);
|
|
412
|
-
const again = await repository
|
|
476
|
+
const again = await repository["findFirst"]({ where: { [primary]: row[primary] } });
|
|
413
477
|
expect(again.ok && again.data === null, "the deleted row is still readable");
|
|
414
478
|
}]
|
|
415
479
|
]);
|
|
@@ -557,7 +621,7 @@ function testGenerator(generator, options) {
|
|
|
557
621
|
hasName(generator),
|
|
558
622
|
["writes files inside the project", async () => {
|
|
559
623
|
const files = await generator.generate(input());
|
|
560
|
-
expect(
|
|
624
|
+
expect(isList(files), "generate() must return an array");
|
|
561
625
|
for (const file of files) {
|
|
562
626
|
expect(typeof file.path === "string" && typeof file.contents === "string", "files need a path and contents");
|
|
563
627
|
expect(!file.path.startsWith("/") && !/^[A-Za-z]:/.test(file.path), `${file.path} must be relative`);
|
|
@@ -587,25 +651,27 @@ function testPlugin(plugin, options) {
|
|
|
587
651
|
schema: sb.meta,
|
|
588
652
|
context,
|
|
589
653
|
options: {},
|
|
590
|
-
now: () =>
|
|
654
|
+
now: () => temporal().Instant.fromEpochMilliseconds(0)
|
|
591
655
|
});
|
|
592
656
|
const create = options.create;
|
|
593
657
|
return conform(`Plugin "${plugin.name}"`, [
|
|
594
658
|
hasName(plugin),
|
|
595
|
-
["targets plugin API v1", () =>
|
|
659
|
+
["targets plugin API v1", () => {
|
|
660
|
+
expect(plugin.apiVersion === 1, `apiVersion is ${String(plugin.apiVersion)}`);
|
|
661
|
+
}],
|
|
596
662
|
["installs and builds repositories", () => {
|
|
597
663
|
const db = sb.use(plugin).connect(recorder([]), context);
|
|
598
664
|
for (const key of Object.keys(sb.meta.tables)) expect(db[key], `db.${key} is missing`);
|
|
599
665
|
}],
|
|
600
666
|
plugin.transformQuery && ["transformQuery is pure and deterministic", async () => {
|
|
601
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
667
|
+
const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
|
|
602
668
|
const first = plugin.transformQuery(frozenCopy(op), hook(op));
|
|
603
669
|
const second = plugin.transformQuery(frozenCopy(op), hook(op));
|
|
604
670
|
expect(typeof first === "object" && first !== null && first.kind === op.kind, "must return an operation of the same kind");
|
|
605
671
|
expect(same(first, second), "two calls with the same input differ");
|
|
606
672
|
}],
|
|
607
673
|
plugin.beforeMutation && create && ["beforeMutation is pure and deterministic", async () => {
|
|
608
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
674
|
+
const op = await captureOp(sb, (db) => db[table]["create"](create));
|
|
609
675
|
if (op.kind === "select") throw new Violation("create did not produce a mutation");
|
|
610
676
|
const first = await plugin.beforeMutation(frozenCopy(op), hook(op));
|
|
611
677
|
const second = await plugin.beforeMutation(frozenCopy(op), hook(op));
|
|
@@ -613,7 +679,7 @@ function testPlugin(plugin, options) {
|
|
|
613
679
|
expect(same(first, second), "two calls with the same input differ");
|
|
614
680
|
}],
|
|
615
681
|
plugin.wrapExecutor && ["wrapExecutor keeps results intact", async () => {
|
|
616
|
-
const op = await captureOp(sb, (db) => db[table]
|
|
682
|
+
const op = await captureOp(sb, (db) => db[table]["findMany"]({ limit: 1 }));
|
|
617
683
|
const data = {
|
|
618
684
|
rows: [{ marker: "conformance" }],
|
|
619
685
|
count: 1
|
|
@@ -664,8 +730,8 @@ function env(names) {
|
|
|
664
730
|
* ```ts
|
|
665
731
|
* await expectTenantIsolation(sb, {
|
|
666
732
|
* tenants: [
|
|
667
|
-
* { id: ACME, claims: { sub: alice,
|
|
668
|
-
* { id: GLOBEX, claims: { sub: bob,
|
|
733
|
+
* { id: ACME, claims: { sub: alice, tenant_id: ACME } },
|
|
734
|
+
* { id: GLOBEX, claims: { sub: bob, tenant_id: GLOBEX } },
|
|
669
735
|
* ],
|
|
670
736
|
* tables: {
|
|
671
737
|
* tags: {
|
|
@@ -748,4 +814,105 @@ async function expectTenantIsolation(sb, options) {
|
|
|
748
814
|
}
|
|
749
815
|
}
|
|
750
816
|
//#endregion
|
|
751
|
-
|
|
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(sb, {
|
|
888
|
+
* organizations: { acme: { id: ACME, name: 'Acme' } },
|
|
889
|
+
* customers: { first: { organizationId: ACME, name: 'First customer' } },
|
|
890
|
+
* });
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
function defineSeed(sb, fixtures) {
|
|
894
|
+
const meta = sb.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
|
|
918
|
+
export { ConformanceError, LOCAL_JWT_SECRET, asUser, createTestSigner, defineSeed, expectDbBudget, expectTenantIsolation, isSeed, localAuth, localSigningKey, signLocalJwt, signTestJwt, signTestJwtWithKey, testAuthResolver, testCacheAdapter, testEventSink, testExecutor, testGenerator, testPlugin };
|