@avelonjs/supabase 0.1.0 → 0.3.1
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 +130 -93
- package/package.json +4 -2
- package/src/database/bun.ts +33 -0
- package/src/database/compile.ts +15 -9
- package/src/database/driver.ts +55 -29
- package/src/database/errors.ts +2 -7
- package/src/database/fixtures.ts +3 -3
- package/src/database/index.ts +2 -1
- package/src/database/normalize.ts +1 -1
- package/src/database/schema-rest.ts +54 -0
- package/src/database/wards.ts +10 -10
- package/src/identity/driver.ts +298 -19
- package/src/identity/errors.ts +230 -0
- package/src/identity/index.ts +1 -0
- package/src/identity/local-auth.ts +145 -23
- package/src/queue/driver.ts +10 -6
- package/src/social/driver.ts +6 -9
- package/src/storage/driver.ts +3 -4
- package/src/tokens/driver.ts +10 -6
package/README.md
CHANGED
|
@@ -39,23 +39,23 @@ await db.execute(published)
|
|
|
39
39
|
|
|
40
40
|
## Capabilities
|
|
41
41
|
|
|
42
|
-
| Capability
|
|
43
|
-
|
|
44
|
-
| `transactions`
|
|
45
|
-
| `rowSecurity`
|
|
46
|
-
| `maxRelationDepth` | `2`
|
|
47
|
-
| `fullTextSearch`
|
|
48
|
-
| `upsert`
|
|
49
|
-
| `returning`
|
|
50
|
-
| `windowFunctions`
|
|
51
|
-
| `jsonOperators`
|
|
42
|
+
| Capability | Value | Notes |
|
|
43
|
+
| ------------------ | ------- | ----------------------------------------------------------------------------- |
|
|
44
|
+
| `transactions` | `false` | PostgREST has no interactive transactions |
|
|
45
|
+
| `rowSecurity` | `true` | `syncWards()` compiles registered wards to RLS |
|
|
46
|
+
| `maxRelationDepth` | `2` | Measured against live PostgREST relation loading |
|
|
47
|
+
| `fullTextSearch` | `false` | No portable `search()` surface in v1 |
|
|
48
|
+
| `upsert` | `true` | Wildcard upserts are native; explicit update lists use `avelon_upsert_subset` |
|
|
49
|
+
| `returning` | `true` | Write queries may project rows |
|
|
50
|
+
| `windowFunctions` | `false` | Not available over PostgREST |
|
|
51
|
+
| `jsonOperators` | `true` | Informational |
|
|
52
52
|
|
|
53
53
|
## Ward Synchronization
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
|
-
import {
|
|
56
|
+
import { createSupabaseBunDatabase } from '@avelonjs/supabase/database/bun'
|
|
57
57
|
|
|
58
|
-
const db =
|
|
58
|
+
const db = createSupabaseBunDatabase({
|
|
59
59
|
wards: [
|
|
60
60
|
{
|
|
61
61
|
name: 'posts_owner_read',
|
|
@@ -76,14 +76,25 @@ await db.syncWards()
|
|
|
76
76
|
|
|
77
77
|
`{ claim: 'uid' }` compiles to `auth.uid()::text`. A live two-identity denial test in this package proves a cross-tenant read is rejected after sync.
|
|
78
78
|
|
|
79
|
+
## Two entries
|
|
80
|
+
|
|
81
|
+
Query traffic goes over PostgREST, so `@avelonjs/supabase/database` carries no `bun` specifier and runs on a Node runtime. Migrations, fixtures and `syncWards()` issue DDL, which PostgREST cannot, so they need a Postgres socket. `@avelonjs/supabase/database/bun` supplies one through Bun's SQL client.
|
|
82
|
+
|
|
83
|
+
| Entry | Request path | Migrations, fixtures, `syncWards()` |
|
|
84
|
+
| --------------------------------- | ------------ | ----------------------------------- |
|
|
85
|
+
| `@avelonjs/supabase/database` | PostgREST | raise `Unavailable` |
|
|
86
|
+
| `@avelonjs/supabase/database/bun` | PostgREST | run over Bun's SQL client |
|
|
87
|
+
|
|
88
|
+
Either way the schema cache the request path validates against comes from PostgREST's own OpenAPI document, filtered by the bearer token the queries use.
|
|
89
|
+
|
|
79
90
|
## Migrations
|
|
80
91
|
|
|
81
|
-
|
|
92
|
+
Migration history uses the same driver-owned SQL pairs as `@avelonjs/postgres`, applied over the admin connection.
|
|
82
93
|
|
|
83
94
|
```ts
|
|
84
|
-
import {
|
|
95
|
+
import { createSupabaseBunDatabase } from '@avelonjs/supabase/database/bun'
|
|
85
96
|
|
|
86
|
-
const db =
|
|
97
|
+
const db = createSupabaseBunDatabase({
|
|
87
98
|
migrations: [
|
|
88
99
|
{
|
|
89
100
|
id: '20260827_create_posts',
|
|
@@ -109,8 +120,10 @@ Fixture provisioning is owned by this package and reloads the PostgREST schema c
|
|
|
109
120
|
|
|
110
121
|
## Identity
|
|
111
122
|
|
|
123
|
+
Next on Node must import `@avelonjs/supabase/identity`. The package root also exports the database driver, which loads Bun SQL.
|
|
124
|
+
|
|
112
125
|
```ts
|
|
113
|
-
import { createSupabaseIdentity } from '@avelonjs/supabase'
|
|
126
|
+
import { createSupabaseIdentity } from '@avelonjs/supabase/identity'
|
|
114
127
|
|
|
115
128
|
const auth = createSupabaseIdentity({
|
|
116
129
|
authUrl: process.env.SUPABASE_AUTH_URL,
|
|
@@ -122,7 +135,23 @@ export async function currentUser(cookies: import('@avelonjs/core').RequestCooki
|
|
|
122
135
|
}
|
|
123
136
|
```
|
|
124
137
|
|
|
125
|
-
Capabilities: `passwords: true`, `magicLinks: true`, `oauth: false`, `organizations: false`, `mfa: []`.
|
|
138
|
+
Capabilities: `passwords: true`, `magicLinks: true`, `oauth: false`, `organizations: false`, `mfa: ['totp']`, `emailVerification: false`. After password sign-in you challenge the enrolled TOTP factor and verify the authenticator code. `challengeMfa()` without an argument uses `totp`. Email confirmation stays on GoTrue's signup flow and is not an Auth method here.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { createSupabaseIdentity } from '@avelonjs/supabase'
|
|
142
|
+
import type { RequestCookies } from '@avelonjs/core'
|
|
143
|
+
|
|
144
|
+
export async function completeTotpChallenge(cookies: RequestCookies) {
|
|
145
|
+
const identity = createSupabaseIdentity({
|
|
146
|
+
authUrl: process.env.SUPABASE_AUTH_URL,
|
|
147
|
+
apiKey: process.env.SUPABASE_ANON_KEY,
|
|
148
|
+
})(cookies)
|
|
149
|
+
|
|
150
|
+
await identity.signInWithPassword('jordan@example.com', 'correct-horse-battery')
|
|
151
|
+
const challenge = await identity.challengeMfa('totp')
|
|
152
|
+
await identity.verifyMfa(challenge.id, '123456')
|
|
153
|
+
}
|
|
154
|
+
```
|
|
126
155
|
|
|
127
156
|
## Social
|
|
128
157
|
|
|
@@ -175,7 +204,7 @@ Transforms are undeclared in v1 (D28). Signed uploads and listing are deferred.
|
|
|
175
204
|
|
|
176
205
|
## Queue
|
|
177
206
|
|
|
178
|
-
Durable jobs use Postgres `FOR UPDATE SKIP LOCKED`.
|
|
207
|
+
Durable jobs use Postgres `FOR UPDATE SKIP LOCKED`. This package names pgmq; this environment does not ship that extension, so skip-locked tables carry the same retry, delay, and dead-letter semantics.
|
|
179
208
|
|
|
180
209
|
```ts
|
|
181
210
|
import { createSupabaseQueue } from '@avelonjs/supabase'
|
|
@@ -189,81 +218,89 @@ await queue.drain(async (receipt) => {
|
|
|
189
218
|
|
|
190
219
|
## Method Reference
|
|
191
220
|
|
|
192
|
-
| Method
|
|
193
|
-
|
|
194
|
-
| `createSupabaseDatabase`
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `SupabaseDatabase.
|
|
199
|
-
| `SupabaseDatabase.
|
|
200
|
-
| `SupabaseDatabase.
|
|
201
|
-
| `SupabaseDatabase.
|
|
202
|
-
| `SupabaseDatabase.
|
|
203
|
-
| `SupabaseDatabase.
|
|
204
|
-
| `SupabaseDatabase.
|
|
205
|
-
| `SupabaseDatabase.
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `SupabaseIdentity.
|
|
211
|
-
| `SupabaseIdentity.
|
|
212
|
-
| `SupabaseIdentity.
|
|
213
|
-
| `SupabaseIdentity.
|
|
214
|
-
| `SupabaseIdentity.
|
|
215
|
-
| `SupabaseIdentity.
|
|
216
|
-
| `SupabaseIdentity.
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
234
|
-
| `
|
|
235
|
-
| `
|
|
236
|
-
| `
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
244
|
-
| `
|
|
245
|
-
| `
|
|
246
|
-
| `
|
|
247
|
-
| `
|
|
248
|
-
| `
|
|
249
|
-
| `
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
255
|
-
| `
|
|
256
|
-
| `SupabaseQueue.
|
|
257
|
-
| `SupabaseQueue.
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
221
|
+
| Method | Signature | Description |
|
|
222
|
+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
223
|
+
| `createSupabaseDatabase` | `(options?: SupabaseDatabaseOptions) => SupabaseDatabase` | Constructs the PostgREST database driver. |
|
|
224
|
+
| `createSupabaseBunDatabase` | `(options?: SupabaseBunDatabaseOptions) => SupabaseDatabase` | Same driver, holding a Bun SQL admin connection for DDL. |
|
|
225
|
+
| `SupabaseBunDatabaseOptions` | `interface` | `SupabaseDatabaseOptions` without `admin`, plus `databaseUrl`. |
|
|
226
|
+
| `loadSchemaCacheFromRest` | `(restUrl: string, headers: Readonly<Record<string, string>>) => Promise<SchemaCache>` | Reads tables and columns from PostgREST's OpenAPI document. |
|
|
227
|
+
| `SupabaseDatabase.execute` | `(query: QueryIR) => Promise<QueryResult>` | Executes IR as the service role. |
|
|
228
|
+
| `SupabaseDatabase.executeAs` | `(token: string, query: QueryIR) => Promise<QueryResult>` | Executes IR as an arbitrary bearer token. |
|
|
229
|
+
| `SupabaseDatabase.rpc` | `(routine: string, args: Readonly<Record<string, unknown>>) => Promise<T>` | Invokes a PostgREST RPC; missing routines raise `Invalid`. |
|
|
230
|
+
| `SupabaseDatabase.plan` | `() => Promise<MigrationPlan>` | Returns pending migration identifiers and SQL steps. |
|
|
231
|
+
| `SupabaseDatabase.apply` | `() => Promise<readonly MigrationStatus[]>` | Applies pending migrations over the admin connection. |
|
|
232
|
+
| `SupabaseDatabase.rollback` | `(steps?: number) => Promise<readonly MigrationStatus[]>` | Rolls back the newest applied migration batches. |
|
|
233
|
+
| `SupabaseDatabase.status` | `() => Promise<readonly MigrationStatus[]>` | Lists applied and pending migration states. |
|
|
234
|
+
| `SupabaseDatabase.syncWards` | `() => Promise<void>` | Applies registered ward policies as Postgres RLS. |
|
|
235
|
+
| `SupabaseDatabase.resetFixtures` | `() => Promise<void>` | Recreates assay fixtures, roles, and helper RPCs. |
|
|
236
|
+
| `SupabaseDatabase.raw` | `() => { restUrl: string }` | Returns the REST root at the vendor boundary. |
|
|
237
|
+
| `SupabaseDatabase.close` | `() => Promise<void>` | Closes the direct Postgres admin client. |
|
|
238
|
+
| `createSupabaseIdentity` | `(options?: SupabaseIdentityOptions) => (cookies: RequestCookies) => SupabaseIdentity` | Returns the pinned config-time identity factory. |
|
|
239
|
+
| `SupabaseIdentity.user` | `() => Promise<SupabaseActor \| null>` | Returns the current actor from the request cookie session. |
|
|
240
|
+
| `SupabaseIdentity.session` | `() => Promise<SupabaseSession \| null>` | Returns the current session or null. |
|
|
241
|
+
| `SupabaseIdentity.register` | `(email: string, password: string) => Promise<SupabaseActor>` | Registers and establishes a session cookie. |
|
|
242
|
+
| `SupabaseIdentity.signInWithPassword` | `(email: string, password: string) => Promise<SupabaseActor>` | Signs in and writes the session cookie. |
|
|
243
|
+
| `SupabaseIdentity.signOut` | `() => Promise<void>` | Ends the Auth session and clears the cookie. |
|
|
244
|
+
| `SupabaseIdentity.sendPasswordReset` | `(email: string) => Promise<void>` | Sends a recovery request without revealing account existence. |
|
|
245
|
+
| `SupabaseIdentity.resetPassword` | `(token: string, password: string) => Promise<void>` | Replaces a password after validating a recovery token. |
|
|
246
|
+
| `SupabaseIdentity.updatePassword` | `(password: string) => Promise<void>` | Changes the current actor's password. |
|
|
247
|
+
| `SupabaseIdentity.sendMagicLink` | `(email: string, redirectTo?: string) => Promise<void>` | Sends a passwordless sign-in link. |
|
|
248
|
+
| `SupabaseIdentity.signInWithMagicLink` | `(token: string) => Promise<SupabaseActor>` | Redeems a link token for a session. |
|
|
249
|
+
| `SupabaseIdentity.challengeMfa` | `(factor?: 'totp') => Promise<MfaChallenge<'totp'>>` | Begins a TOTP challenge for the current session. |
|
|
250
|
+
| `SupabaseIdentity.verifyMfa` | `(challengeId: string, code: string) => Promise<void>` | Verifies a TOTP challenge response. |
|
|
251
|
+
| `SupabaseIdentity.raw` | `() => { authUrl: string }` | Returns the Auth HTTP root at the vendor boundary. |
|
|
252
|
+
| `readSessionCookie` | `(cookies: RequestCookies, cookieName: string) => SupabaseSession \| null` | Parses the request-scoped session cookie. |
|
|
253
|
+
| `writeSessionCookie` | `(cookies: RequestCookies, cookieName: string, session: SupabaseSession) => void` | Writes the session cookie for the current request. |
|
|
254
|
+
| `clearSessionCookie` | `(cookies: RequestCookies, cookieName: string) => void` | Deletes the session cookie for the current request. |
|
|
255
|
+
| `LocalAuthServer.start` | `() => Promise<string>` | Starts a GoTrue-shaped Auth fixture on an ephemeral port. |
|
|
256
|
+
| `LocalAuthServer.reset` | `() => void` | Clears users, sessions, and recovery tokens. |
|
|
257
|
+
| `LocalAuthServer.stop` | `() => Promise<void>` | Stops the fixture listener. |
|
|
258
|
+
| `LocalAuthServer.recoveryToken` | `(email: string) => string \| undefined` | Returns the recovery token issued for an email. |
|
|
259
|
+
| `LocalAuthServer.magicLinkToken` | `(email: string) => string \| undefined` | Returns the magic-link token issued for an email. |
|
|
260
|
+
| `SUPABASE_IDENTITY_ERROR_MAP` | `readonly { code: string; framework: string; meaning: string }[]` | GoTrue error codes and the taxonomy member each becomes. |
|
|
261
|
+
| `createSupabaseSocial` | `(options?: SupabaseSocialOptions) => SupabaseSocial` | Constructs the GoTrue social driver. |
|
|
262
|
+
| `SupabaseSocial.redirect` | `(provider: string, callbackUrl: string, state?: string) => Promise<string>` | Builds a GoTrue authorize URL and records CSRF state. |
|
|
263
|
+
| `SupabaseSocial.callback` | `(provider: string, params: Readonly<Record<string, string>>, callbackUrl: string) => Promise<SocialIdentity>` | Verifies state and exchanges the authorization code. |
|
|
264
|
+
| `SupabaseSocial.raw` | `() => { authUrl: string }` | Returns the Auth HTTP root at the vendor boundary. |
|
|
265
|
+
| `LocalSocialServer.start` | `() => Promise<string>` | Starts a GoTrue-shaped token fixture on an ephemeral port. |
|
|
266
|
+
| `LocalSocialServer.stop` | `() => Promise<void>` | Stops the social fixture listener. |
|
|
267
|
+
| `createSupabaseTokens` | `(options?: SupabaseTokenOptions) => SupabaseTokens` | Constructs the Postgres-backed Signet driver. |
|
|
268
|
+
| `SupabaseTokens.issue` | `(name: string, options?: TokenIssueOptions) => Promise<IssuedToken>` | Issues a named token and returns plaintext once. |
|
|
269
|
+
| `SupabaseTokens.verify` | `(plainText: string) => Promise<TokenRecord>` | Authenticates a plaintext token or raises `Unauthenticated`. |
|
|
270
|
+
| `SupabaseTokens.list` | `() => Promise<readonly TokenRecord[]>` | Lists metadata for the current subject without plaintext. |
|
|
271
|
+
| `SupabaseTokens.revoke` | `(id: string) => Promise<void>` | Revokes one token by stable identifier. |
|
|
272
|
+
| `SupabaseTokens.reset` | `() => Promise<void>` | Recreates the empty signet table. |
|
|
273
|
+
| `SupabaseTokens.raw` | `() => SQL` | Returns the Bun SQL client at the vendor boundary. |
|
|
274
|
+
| `SupabaseTokens.close` | `() => Promise<void>` | Closes the SQL client pool. |
|
|
275
|
+
| `createSupabaseStorage` | `(options?: SupabaseStorageOptions) => SupabaseStorage` | Constructs the Postgres-backed storage driver. |
|
|
276
|
+
| `SupabaseStorage.put` | `(path: string, contents: Uint8Array \| AsyncIterable<Uint8Array>, options?: { contentType?: string }) => Promise<StorageObject>` | Stores bytes and returns metadata. |
|
|
277
|
+
| `SupabaseStorage.get` | `(path: string) => Promise<Uint8Array>` | Reads object bytes or raises `NotFound`. |
|
|
278
|
+
| `SupabaseStorage.delete` | `(path: string) => Promise<void>` | Deletes an object if it exists. |
|
|
279
|
+
| `SupabaseStorage.exists` | `(path: string) => Promise<boolean>` | Reports whether an object exists. |
|
|
280
|
+
| `SupabaseStorage.signedUrl` | `(path: string, expiresInSeconds: number) => Promise<string>` | Creates a fetchable HMAC-signed read URL. |
|
|
281
|
+
| `SupabaseStorage.reset` | `() => Promise<void>` | Recreates the empty object table. |
|
|
282
|
+
| `SupabaseStorage.raw` | `() => SQL` | Returns the Bun SQL client at the vendor boundary. |
|
|
283
|
+
| `SupabaseStorage.close` | `() => Promise<void>` | Stops the signed-URL listener and closes SQL. |
|
|
284
|
+
| `createSupabaseQueue` | `(options?: SupabaseQueueOptions) => SupabaseQueue` | Constructs the skip-locked Postgres queue driver. |
|
|
285
|
+
| `SupabaseQueue.enqueue` | `(job: QueueJob) => Promise<string>` | Enqueues a job for immediate delivery. |
|
|
286
|
+
| `SupabaseQueue.enqueueAt` | `(job: QueueJob, availableAt: Date) => Promise<string>` | Enqueues a job no earlier than the supplied time. |
|
|
287
|
+
| `SupabaseQueue.drain` | `(handler: (receipt: QueueReceipt) => Promise<void>, options?: { queue?: string; limit?: number }) => Promise<number>` | Delivers available jobs and counts attempts. |
|
|
288
|
+
| `SupabaseQueue.retry` | `(id: string, delaySeconds?: number) => Promise<void>` | Releases a failed job, optionally after a delay. |
|
|
289
|
+
| `SupabaseQueue.failed` | `(queue?: string) => Promise<readonly FailedQueueJob[]>` | Lists retained terminal failures. |
|
|
290
|
+
| `SupabaseQueue.replay` | `(id: string) => Promise<void>` | Replays a terminal failure with a fresh attempt budget. |
|
|
291
|
+
| `SupabaseQueue.forget` | `(id: string) => Promise<void>` | Permanently removes a terminal failure. |
|
|
292
|
+
| `SupabaseQueue.reset` | `() => Promise<void>` | Recreates empty job tables. |
|
|
293
|
+
| `SupabaseQueue.raw` | `() => SQL` | Returns the Bun SQL client at the vendor boundary. |
|
|
294
|
+
| `SupabaseQueue.close` | `() => Promise<void>` | Closes the SQL client pool. |
|
|
295
|
+
| `compilePostgrest` | `(ir: QueryIR, predicate?: Predicate) => CompiledPostgrestRequest` | Compiles IR to an HTTP request. |
|
|
296
|
+
| `applyPostgrestPredicate` | `(parameters: URLSearchParams, predicate: Predicate) => void` | Writes a normalized predicate into PostgREST `and=` filters. |
|
|
297
|
+
| `compileWardPredicate` | `(predicate: Predicate \| boolean) => string` | Compiles a ward predicate to RLS SQL. |
|
|
298
|
+
| `compileWardPolicySql` | `(policy: WardPolicy) => string[]` | Builds DROP/CREATE POLICY statements for one ward. |
|
|
299
|
+
| `compileAllWardPolicies` | `(policies: readonly WardPolicy[]) => string[]` | Compiles every registered ward into ordered SQL. |
|
|
300
|
+
| `mapPostgrestError` | `(status: number, bodyText: string, operation: string) => never` | Maps PostgREST failures into framework errors. |
|
|
301
|
+
| `resetSupabaseAssayFixtures` | `(sql: SQL) => Promise<void>` | Provisions empty assay fixtures on a SQL client. |
|
|
302
|
+
| `normalizePredicate` | `(predicate: Predicate) => Predicate` | Applies empty-list and constant identities. |
|
|
303
|
+
| `combinedPredicate` | `(ir: Pick<QueryIR, 'where' \| 'ward'>) => Predicate` | ANDs where and ward, then normalizes. |
|
|
267
304
|
|
|
268
305
|
## Testing
|
|
269
306
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avelonjs/supabase",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Supabase drivers for Avelon's database, identity, social, tokens, storage, and queue contracts.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
],
|
|
32
32
|
"exports": {
|
|
33
33
|
".": "./src/index.ts",
|
|
34
|
-
"./database": "./src/database/index.ts"
|
|
34
|
+
"./database": "./src/database/index.ts",
|
|
35
|
+
"./database/bun": "./src/database/bun.ts",
|
|
36
|
+
"./identity": "./src/identity/driver.ts"
|
|
35
37
|
},
|
|
36
38
|
"scripts": {
|
|
37
39
|
"test": "bun test",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SQL } from 'bun'
|
|
2
|
+
import { bunSqlRunner } from '@avelonjs/postgres'
|
|
3
|
+
import { SupabaseDatabase, type SupabaseDatabaseOptions } from './driver'
|
|
4
|
+
|
|
5
|
+
/** Construction options for a driver that also holds a direct Postgres connection. */
|
|
6
|
+
export interface SupabaseBunDatabaseOptions extends Omit<SupabaseDatabaseOptions, 'admin'> {
|
|
7
|
+
/** Direct Postgres URL for migrations, fixtures and ward sync. */
|
|
8
|
+
databaseUrl?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultDatabaseUrl(): string {
|
|
12
|
+
return (
|
|
13
|
+
process.env.SUPABASE_DB_URL ??
|
|
14
|
+
process.env.DATABASE_URL ??
|
|
15
|
+
'postgresql://postgres:avelon@127.0.0.1:5432/avelon_supabase'
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds a Supabase driver whose migrations, fixtures and ward sync run over Bun's SQL client.
|
|
21
|
+
*
|
|
22
|
+
* The request path is the same PostgREST code either way. Only the operations needing a Postgres
|
|
23
|
+
* socket differ, which is why they live behind this entry rather than the runtime one.
|
|
24
|
+
*/
|
|
25
|
+
export function createSupabaseBunDatabase(
|
|
26
|
+
options: SupabaseBunDatabaseOptions = {},
|
|
27
|
+
): SupabaseDatabase {
|
|
28
|
+
const { databaseUrl, ...rest } = options
|
|
29
|
+
return new SupabaseDatabase({
|
|
30
|
+
...rest,
|
|
31
|
+
admin: bunSqlRunner(new SQL(databaseUrl ?? defaultDatabaseUrl())),
|
|
32
|
+
})
|
|
33
|
+
}
|
package/src/database/compile.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Invalid, type CompareOp, type Predicate, type QueryIR } from '@avelonjs/core'
|
|
2
|
-
import { combinedPredicate } from '@avelonjs/postgres'
|
|
2
|
+
import { combinedPredicate } from '@avelonjs/postgres/sql'
|
|
3
3
|
|
|
4
4
|
const POSTGREST_OPERATOR: Record<CompareOp, string> = {
|
|
5
5
|
'=': 'eq',
|
|
@@ -37,12 +37,17 @@ function postgrestProjection(projection: string[] | '*'): string {
|
|
|
37
37
|
function postgrestScalar(value: unknown): string {
|
|
38
38
|
if (typeof value === 'string') return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`
|
|
39
39
|
if (typeof value === 'number') {
|
|
40
|
-
if (!Number.isFinite(value))
|
|
40
|
+
if (!Number.isFinite(value))
|
|
41
|
+
invalid('PostgREST cannot encode a non-finite number.', 'value', 'Invalid number')
|
|
41
42
|
return String(value)
|
|
42
43
|
}
|
|
43
44
|
if (typeof value === 'boolean') return String(value)
|
|
44
45
|
if (value === null) return 'null'
|
|
45
|
-
invalid(
|
|
46
|
+
invalid(
|
|
47
|
+
`PostgREST scalar type '${typeof value}' is not supported.`,
|
|
48
|
+
'value',
|
|
49
|
+
'Unsupported scalar',
|
|
50
|
+
)
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
function postgrestLeaf(
|
|
@@ -69,7 +74,8 @@ function postgrestLeaf(
|
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
function postgrestExpression(predicate: Predicate, negated = false): string {
|
|
72
|
-
if (predicate.kind === 'const')
|
|
77
|
+
if (predicate.kind === 'const')
|
|
78
|
+
invalid('predicate was not normalized for PostgREST.', 'where', 'Unexpected const')
|
|
73
79
|
if (predicate.kind === 'not') return postgrestExpression(predicate.predicate, !negated)
|
|
74
80
|
if (predicate.kind === 'and' || predicate.kind === 'or') {
|
|
75
81
|
const kind = negated ? (predicate.kind === 'and' ? 'or' : 'and') : predicate.kind
|
|
@@ -168,7 +174,10 @@ export function compilePostgrest(
|
|
|
168
174
|
ensureUnscopedWritePredicate(predicate, 'upsert')
|
|
169
175
|
method = 'POST'
|
|
170
176
|
body = JSON.stringify(ir.values)
|
|
171
|
-
parameters.set(
|
|
177
|
+
parameters.set(
|
|
178
|
+
'on_conflict',
|
|
179
|
+
(ir.conflict as NonNullable<QueryIR['conflict']>).columns.join(','),
|
|
180
|
+
)
|
|
172
181
|
headers.Prefer = 'resolution=merge-duplicates'
|
|
173
182
|
}
|
|
174
183
|
|
|
@@ -182,10 +191,7 @@ export function compilePostgrest(
|
|
|
182
191
|
// Always request representation so `affected` is observable; the driver drops rows when
|
|
183
192
|
// `returning` is absent.
|
|
184
193
|
headers.Prefer = [headers.Prefer, 'return=representation'].filter(Boolean).join(',')
|
|
185
|
-
parameters.set(
|
|
186
|
-
'select',
|
|
187
|
-
postgrestProjection(ir.returning === undefined ? '*' : ir.returning),
|
|
188
|
-
)
|
|
194
|
+
parameters.set('select', postgrestProjection(ir.returning === undefined ? '*' : ir.returning))
|
|
189
195
|
}
|
|
190
196
|
|
|
191
197
|
return {
|