@aurabase/aurabase-js 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/.changeset/config.json +11 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +833 -0
- package/package.json +90 -0
- package/packages/ai/LICENSE +21 -0
- package/packages/ai/README.md +106 -0
- package/packages/ai/dist/index.cjs +229 -0
- package/packages/ai/dist/index.d.cts +139 -0
- package/packages/ai/dist/index.d.ts +139 -0
- package/packages/ai/dist/index.js +226 -0
- package/packages/ai/node_modules/.bin/esbuild +19 -0
- package/packages/ai/node_modules/.bin/tsc +22 -0
- package/packages/ai/node_modules/.bin/tsserver +22 -0
- package/packages/ai/node_modules/.bin/tsup +22 -0
- package/packages/ai/node_modules/.bin/tsup-node +22 -0
- package/packages/ai/node_modules/.bin/vite +22 -0
- package/packages/ai/node_modules/.bin/vitest +22 -0
- package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/ai/package.json +61 -0
- package/packages/ai/src/AiService.ts +226 -0
- package/packages/ai/src/index.ts +18 -0
- package/packages/ai/src/sse.ts +141 -0
- package/packages/ai/src/types.ts +30 -0
- package/packages/ai/tests/chat-stream.test.ts +151 -0
- package/packages/ai/tests/rag-namespaces.test.ts +54 -0
- package/packages/ai/tests/sse.test.ts +69 -0
- package/packages/ai/tsconfig.json +5 -0
- package/packages/ai/tsup.config.ts +11 -0
- package/packages/aurabase/LICENSE +21 -0
- package/packages/aurabase/README.md +64 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
- package/packages/aurabase/dist/index.cjs +237 -0
- package/packages/aurabase/dist/index.d.cts +46 -0
- package/packages/aurabase/dist/index.d.ts +46 -0
- package/packages/aurabase/dist/index.js +177 -0
- package/packages/aurabase/dist/ssr.cjs +224 -0
- package/packages/aurabase/dist/ssr.d.cts +65 -0
- package/packages/aurabase/dist/ssr.d.ts +65 -0
- package/packages/aurabase/dist/ssr.js +214 -0
- package/packages/aurabase/node_modules/.bin/esbuild +19 -0
- package/packages/aurabase/node_modules/.bin/tsc +22 -0
- package/packages/aurabase/node_modules/.bin/tsserver +22 -0
- package/packages/aurabase/node_modules/.bin/tsup +22 -0
- package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
- package/packages/aurabase/node_modules/.bin/vite +22 -0
- package/packages/aurabase/node_modules/.bin/vitest +22 -0
- package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/aurabase/package.json +80 -0
- package/packages/aurabase/src/AuraClient.ts +237 -0
- package/packages/aurabase/src/index.ts +152 -0
- package/packages/aurabase/src/ssr.ts +156 -0
- package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
- package/packages/aurabase/tests/ai.test.ts +111 -0
- package/packages/aurabase/tests/auth-admin.test.ts +515 -0
- package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
- package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
- package/packages/aurabase/tests/auth.test.ts +947 -0
- package/packages/aurabase/tests/database.test.ts +230 -0
- package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
- package/packages/aurabase/tests/dispose.test.ts +23 -0
- package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
- package/packages/aurabase/tests/functions.test.ts +107 -0
- package/packages/aurabase/tests/integration.test.ts +413 -0
- package/packages/aurabase/tests/live-production-validation.js +241 -0
- package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
- package/packages/aurabase/tests/mocks.ts +139 -0
- package/packages/aurabase/tests/notifications.test.ts +314 -0
- package/packages/aurabase/tests/observability-backup.test.ts +186 -0
- package/packages/aurabase/tests/observability-hook.test.ts +130 -0
- package/packages/aurabase/tests/production-robustness.test.ts +268 -0
- package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
- package/packages/aurabase/tests/query-builder.test.ts +329 -0
- package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
- package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
- package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
- package/packages/aurabase/tests/retry-option.test.ts +17 -0
- package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
- package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
- package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
- package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
- package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
- package/packages/aurabase/tests/ssr.test.ts +91 -0
- package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
- package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
- package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
- package/packages/aurabase/tests/storage.test.ts +55 -0
- package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
- package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
- package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
- package/packages/aurabase/tsconfig.json +5 -0
- package/packages/aurabase/tsconfig.test-d.json +5 -0
- package/packages/aurabase/tsup.config.ts +11 -0
- package/packages/aurabase/vitest.config.ts +16 -0
- package/packages/aurabase/vitest.e2e.config.ts +9 -0
- package/packages/auth/LICENSE +21 -0
- package/packages/auth/README.md +215 -0
- package/packages/auth/dist/index.cjs +1175 -0
- package/packages/auth/dist/index.d.cts +397 -0
- package/packages/auth/dist/index.d.ts +397 -0
- package/packages/auth/dist/index.js +1170 -0
- package/packages/auth/node_modules/.bin/esbuild +19 -0
- package/packages/auth/node_modules/.bin/tsc +22 -0
- package/packages/auth/node_modules/.bin/tsserver +22 -0
- package/packages/auth/node_modules/.bin/tsup +22 -0
- package/packages/auth/node_modules/.bin/tsup-node +22 -0
- package/packages/auth/node_modules/.bin/vite +22 -0
- package/packages/auth/node_modules/.bin/vitest +22 -0
- package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/auth/package.json +61 -0
- package/packages/auth/src/AuthAdminService.ts +149 -0
- package/packages/auth/src/AuthService.ts +1166 -0
- package/packages/auth/src/SessionManager.ts +92 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/auth/src/jwks.ts +337 -0
- package/packages/auth/src/pkce.ts +27 -0
- package/packages/auth/src/types.ts +89 -0
- package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
- package/packages/auth/tests/detect-session.test.ts +361 -0
- package/packages/auth/tests/getclaims.test.ts +780 -0
- package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
- package/packages/auth/tests/password-recovery.test.ts +124 -0
- package/packages/auth/tests/pkce.test.ts +192 -0
- package/packages/auth/tests/session-manager.test.ts +103 -0
- package/packages/auth/tests/set-session.test.ts +139 -0
- package/packages/auth/tests/verify-otp.test.ts +153 -0
- package/packages/auth/tsconfig.json +5 -0
- package/packages/auth/tsup.config.ts +11 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +132 -0
- package/packages/core/dist/index.cjs +695 -0
- package/packages/core/dist/index.d.cts +1137 -0
- package/packages/core/dist/index.d.ts +1137 -0
- package/packages/core/dist/index.js +673 -0
- package/packages/core/node_modules/.bin/esbuild +22 -0
- package/packages/core/node_modules/.bin/tsc +22 -0
- package/packages/core/node_modules/.bin/tsserver +22 -0
- package/packages/core/node_modules/.bin/tsup +22 -0
- package/packages/core/node_modules/.bin/tsup-node +22 -0
- package/packages/core/node_modules/.bin/vite +22 -0
- package/packages/core/node_modules/.bin/vitest +22 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +57 -0
- package/packages/core/src/AuraError.ts +89 -0
- package/packages/core/src/BaseService.ts +44 -0
- package/packages/core/src/client.ts +34 -0
- package/packages/core/src/cookie-utils.ts +42 -0
- package/packages/core/src/fetch.ts +435 -0
- package/packages/core/src/index.ts +22 -0
- package/packages/core/src/retry.ts +63 -0
- package/packages/core/src/serialize.ts +6 -0
- package/packages/core/src/stores/BaseAuthStore.ts +164 -0
- package/packages/core/src/stores/LocalAuthStore.ts +199 -0
- package/packages/core/src/types.ts +1042 -0
- package/packages/core/tests/auth-store.test.ts +300 -0
- package/packages/core/tests/error-code.test.ts +61 -0
- package/packages/core/tests/error.test.ts +24 -0
- package/packages/core/tests/fetch.test.ts +286 -0
- package/packages/core/tests/mocks.ts +139 -0
- package/packages/core/tests/observability.test.ts +86 -0
- package/packages/core/tests/refresh-hardening.test.ts +164 -0
- package/packages/core/tests/refresh-observability.test.ts +111 -0
- package/packages/core/tests/retry.test.ts +106 -0
- package/packages/core/tests/serialize.test.ts +12 -0
- package/packages/core/tests/timeout.test.ts +79 -0
- package/packages/core/tsconfig.json +5 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/db/LICENSE +21 -0
- package/packages/db/README.md +152 -0
- package/packages/db/VENDORED.md +93 -0
- package/packages/db/dist/index.cjs +544 -0
- package/packages/db/dist/index.d.cts +1111 -0
- package/packages/db/dist/index.d.ts +1111 -0
- package/packages/db/dist/index.js +541 -0
- package/packages/db/node_modules/.bin/esbuild +19 -0
- package/packages/db/node_modules/.bin/tsc +22 -0
- package/packages/db/node_modules/.bin/tsserver +22 -0
- package/packages/db/node_modules/.bin/tsup +22 -0
- package/packages/db/node_modules/.bin/tsup-node +22 -0
- package/packages/db/node_modules/.bin/vite +22 -0
- package/packages/db/node_modules/.bin/vitest +22 -0
- package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/db/package.json +62 -0
- package/packages/db/src/DatabaseService.ts +170 -0
- package/packages/db/src/QueryBuilder.ts +830 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/pgrst-types/common/common.ts +86 -0
- package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
- package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
- package/packages/db/src/pgrst-types/types.ts +134 -0
- package/packages/db/src/select-query-parser/parser.ts +479 -0
- package/packages/db/src/select-query-parser/result.ts +553 -0
- package/packages/db/src/select-query-parser/types.ts +132 -0
- package/packages/db/src/select-query-parser/utils.ts +707 -0
- package/packages/db/tests/filters.test.ts +81 -0
- package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
- package/packages/db/tests/mutations-filtered.test.ts +188 -0
- package/packages/db/tests/order.test.ts +45 -0
- package/packages/db/tests/rpc.test.ts +41 -0
- package/packages/db/tests/scale/build-large-schema.ts +94 -0
- package/packages/db/tests/scale/scale-compile.test.ts +167 -0
- package/packages/db/tests/schema.test.ts +77 -0
- package/packages/db/tests/single-accept.e2e.test.ts +123 -0
- package/packages/db/tests/single.test.ts +211 -0
- package/packages/db/tests/types/filters.test-d.ts +52 -0
- package/packages/db/tests/types/fixtures.ts +40 -0
- package/packages/db/tests/types/from-views.test-d.ts +18 -0
- package/packages/db/tests/types/mutations.test-d.ts +23 -0
- package/packages/db/tests/types/rpc.test-d.ts +60 -0
- package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
- package/packages/db/tests/types/select-inference.test-d.ts +38 -0
- package/packages/db/tests/vendored/fixtures.ts +83 -0
- package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
- package/packages/db/tsconfig.json +5 -0
- package/packages/db/tsconfig.test-d.json +5 -0
- package/packages/db/tsup.config.ts +11 -0
- package/packages/db/vitest.config.ts +8 -0
- package/packages/functions/LICENSE +21 -0
- package/packages/functions/README.md +96 -0
- package/packages/functions/dist/index.cjs +173 -0
- package/packages/functions/dist/index.d.cts +81 -0
- package/packages/functions/dist/index.d.ts +81 -0
- package/packages/functions/dist/index.js +168 -0
- package/packages/functions/node_modules/.bin/esbuild +19 -0
- package/packages/functions/node_modules/.bin/tsc +22 -0
- package/packages/functions/node_modules/.bin/tsserver +22 -0
- package/packages/functions/node_modules/.bin/tsup +22 -0
- package/packages/functions/node_modules/.bin/tsup-node +22 -0
- package/packages/functions/node_modules/.bin/vite +22 -0
- package/packages/functions/node_modules/.bin/vitest +22 -0
- package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/functions/package.json +60 -0
- package/packages/functions/src/FunctionsService.ts +188 -0
- package/packages/functions/src/errors.ts +70 -0
- package/packages/functions/src/index.ts +6 -0
- package/packages/functions/tests/invoke.test.ts +97 -0
- package/packages/functions/tests/response-type-errors.test.ts +135 -0
- package/packages/functions/tsconfig.json +5 -0
- package/packages/functions/tsup.config.ts +11 -0
- package/packages/gen/LICENSE +21 -0
- package/packages/gen/README.md +84 -0
- package/packages/gen/dist/cli.cjs +334 -0
- package/packages/gen/dist/cli.d.cts +1 -0
- package/packages/gen/dist/cli.d.ts +1 -0
- package/packages/gen/dist/cli.js +332 -0
- package/packages/gen/dist/index.cjs +257 -0
- package/packages/gen/dist/index.d.cts +12 -0
- package/packages/gen/dist/index.d.ts +12 -0
- package/packages/gen/dist/index.js +255 -0
- package/packages/gen/node_modules/.bin/esbuild +19 -0
- package/packages/gen/node_modules/.bin/tsc +22 -0
- package/packages/gen/node_modules/.bin/tsserver +22 -0
- package/packages/gen/node_modules/.bin/tsup +22 -0
- package/packages/gen/node_modules/.bin/tsup-node +22 -0
- package/packages/gen/node_modules/.bin/vite +22 -0
- package/packages/gen/node_modules/.bin/vitest +22 -0
- package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/gen/package.json +66 -0
- package/packages/gen/src/cli.ts +67 -0
- package/packages/gen/src/generate.ts +0 -0
- package/packages/gen/src/index.ts +2 -0
- package/packages/gen/tests/compile.test.ts +213 -0
- package/packages/gen/tests/e2e.test.ts +130 -0
- package/packages/gen/tests/generate.test.ts +396 -0
- package/packages/gen/tsconfig.json +5 -0
- package/packages/gen/tsup.config.ts +11 -0
- package/packages/notifications/LICENSE +21 -0
- package/packages/notifications/README.md +134 -0
- package/packages/notifications/dist/index.cjs +128 -0
- package/packages/notifications/dist/index.d.cts +81 -0
- package/packages/notifications/dist/index.d.ts +81 -0
- package/packages/notifications/dist/index.js +125 -0
- package/packages/notifications/node_modules/.bin/esbuild +19 -0
- package/packages/notifications/node_modules/.bin/tsc +22 -0
- package/packages/notifications/node_modules/.bin/tsserver +22 -0
- package/packages/notifications/node_modules/.bin/tsup +22 -0
- package/packages/notifications/node_modules/.bin/tsup-node +22 -0
- package/packages/notifications/node_modules/.bin/vite +22 -0
- package/packages/notifications/node_modules/.bin/vitest +22 -0
- package/packages/notifications/package.json +60 -0
- package/packages/notifications/src/NotificationsService.ts +230 -0
- package/packages/notifications/src/index.ts +19 -0
- package/packages/notifications/tsconfig.json +5 -0
- package/packages/notifications/tsup.config.ts +11 -0
- package/packages/realtime/LICENSE +21 -0
- package/packages/realtime/README.md +183 -0
- package/packages/realtime/dist/index.cjs +977 -0
- package/packages/realtime/dist/index.d.cts +259 -0
- package/packages/realtime/dist/index.d.ts +259 -0
- package/packages/realtime/dist/index.js +973 -0
- package/packages/realtime/node_modules/.bin/esbuild +19 -0
- package/packages/realtime/node_modules/.bin/tsc +22 -0
- package/packages/realtime/node_modules/.bin/tsserver +22 -0
- package/packages/realtime/node_modules/.bin/tsup +22 -0
- package/packages/realtime/node_modules/.bin/tsup-node +22 -0
- package/packages/realtime/node_modules/.bin/vite +22 -0
- package/packages/realtime/node_modules/.bin/vitest +22 -0
- package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/realtime/package.json +60 -0
- package/packages/realtime/src/AuraChannel.ts +485 -0
- package/packages/realtime/src/HeartbeatManager.ts +73 -0
- package/packages/realtime/src/RealtimeService.ts +505 -0
- package/packages/realtime/src/columnFilter.ts +301 -0
- package/packages/realtime/src/index.ts +4 -0
- package/packages/realtime/tests/ack-routing.test.ts +81 -0
- package/packages/realtime/tests/column-filter.test.ts +144 -0
- package/packages/realtime/tests/dispatch.test.ts +191 -0
- package/packages/realtime/tests/heartbeat.test.ts +67 -0
- package/packages/realtime/tests/http-broadcast.test.ts +68 -0
- package/packages/realtime/tests/realtime-token.test.ts +43 -0
- package/packages/realtime/tests/reconnect-race.test.ts +149 -0
- package/packages/realtime/tests/ref-id.test.ts +34 -0
- package/packages/realtime/tests/set-auth.test.ts +70 -0
- package/packages/realtime/tsconfig.json +5 -0
- package/packages/realtime/tsup.config.ts +11 -0
- package/packages/storage/LICENSE +21 -0
- package/packages/storage/README.md +141 -0
- package/packages/storage/dist/index.cjs +333 -0
- package/packages/storage/dist/index.d.cts +185 -0
- package/packages/storage/dist/index.d.ts +185 -0
- package/packages/storage/dist/index.js +329 -0
- package/packages/storage/node_modules/.bin/esbuild +19 -0
- package/packages/storage/node_modules/.bin/tsc +22 -0
- package/packages/storage/node_modules/.bin/tsserver +22 -0
- package/packages/storage/node_modules/.bin/tsup +22 -0
- package/packages/storage/node_modules/.bin/tsup-node +22 -0
- package/packages/storage/node_modules/.bin/vite +22 -0
- package/packages/storage/node_modules/.bin/vitest +22 -0
- package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/storage/package.json +60 -0
- package/packages/storage/src/DownloadBuilder.ts +60 -0
- package/packages/storage/src/StorageService.ts +323 -0
- package/packages/storage/src/index.ts +2 -0
- package/packages/storage/src/path.ts +35 -0
- package/packages/storage/src/transform.ts +15 -0
- package/packages/storage/tests/download.test.ts +64 -0
- package/packages/storage/tests/parity.test.ts +154 -0
- package/packages/storage/tests/path.test.ts +87 -0
- package/packages/storage/tests/transform.test.ts +79 -0
- package/packages/storage/tsconfig.json +5 -0
- package/packages/storage/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +24 -0
- package/tsconfig.base.json +15 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import type { AuraResponse, CountMode, AuraClientLike } from '@aurabase/core'
|
|
2
|
+
import { fetchWithAuth, AuraError } from '@aurabase/core'
|
|
3
|
+
import type { GetResult } from './select-query-parser/result'
|
|
4
|
+
import type {
|
|
5
|
+
GenericSchema,
|
|
6
|
+
GenericTable,
|
|
7
|
+
GenericView,
|
|
8
|
+
ClientServerOptions,
|
|
9
|
+
} from './pgrst-types/common/common'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `ClientOptions` figé pour aurabase — PostgREST v12. Satisfait `ClientServerOptions`
|
|
13
|
+
* et sert de défaut au threading de types (pas de configuration exposée côté SDK).
|
|
14
|
+
*/
|
|
15
|
+
export type AuraClientOptionsT = { PostgrestVersion: '12' }
|
|
16
|
+
|
|
17
|
+
interface StoredFilter {
|
|
18
|
+
column: string
|
|
19
|
+
op: string
|
|
20
|
+
value: unknown
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Quote une valeur PostgREST contenant des caractères réservés (`,` `(` `)` `"`).
|
|
25
|
+
* Sans ça, une valeur comme `"Aix, FR"` dans un `in.(…)` serait lue comme deux valeurs.
|
|
26
|
+
* Les `"` internes sont doublés (échappement PostgREST).
|
|
27
|
+
*/
|
|
28
|
+
function quoteReserved(v: unknown): string {
|
|
29
|
+
const s = String(v)
|
|
30
|
+
return /[,()"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Encode une valeur de filtre en fragment PostgREST `op.value` (gère array-syntax, jsonb, ranges). */
|
|
34
|
+
function encodeFilterValue(op: string, value: unknown): string {
|
|
35
|
+
if (op === 'in' || op === 'not.in') {
|
|
36
|
+
const arr = Array.isArray(value) ? value : [value]
|
|
37
|
+
return `${op}.(${arr.map(quoteReserved).join(',')})`
|
|
38
|
+
}
|
|
39
|
+
if (op === 'cs' || op === 'cd' || op === 'ov') {
|
|
40
|
+
if (Array.isArray(value)) return `${op}.{${value.map(quoteReserved).join(',')}}`
|
|
41
|
+
if (value !== null && typeof value === 'object') return `${op}.${JSON.stringify(value)}`
|
|
42
|
+
return `${op}.${value}`
|
|
43
|
+
}
|
|
44
|
+
if (op.endsWith('(all)') || op.endsWith('(any)')) {
|
|
45
|
+
const arr = Array.isArray(value) ? value : [value]
|
|
46
|
+
return `${op}.{${arr.map(quoteReserved).join(',')}}`
|
|
47
|
+
}
|
|
48
|
+
return `${op}.${value}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface OrGroup {
|
|
52
|
+
filters: StoredFilter[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** En-tête PostgREST demandant un objet unique (parité postgrest-js `.single()`). */
|
|
56
|
+
const PGRST_OBJECT_ACCEPT = 'application/vnd.pgrst.object+json'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Détecte un 406 « 0 lignes » (vs « plusieurs lignes ») pour discriminer les deux cas
|
|
60
|
+
* de cardinalité de `single()`/`maybeSingle()`. Le gateway réécrit PGRST116 en code
|
|
61
|
+
* `NOT_SINGULAR` mais préserve le détail PostgREST « The result contains N rows » (imbriqué
|
|
62
|
+
* sous `error.details`). On cherche donc « 0 rows » dans message + détails sérialisés.
|
|
63
|
+
* `\b0 rows\b` évite un faux positif sur « 10 rows », « 20 rows », etc.
|
|
64
|
+
*/
|
|
65
|
+
function mentionsZeroRows(err: { message?: string; details?: unknown } | null): boolean {
|
|
66
|
+
if (!err) return false
|
|
67
|
+
const details = typeof err.details === 'string' ? err.details : JSON.stringify(err.details ?? '')
|
|
68
|
+
return /\b0 rows\b/.test(`${err.message ?? ''} ${details}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Construit l'erreur de cardinalité PGRST116 (parité postgrest-js/Supabase). */
|
|
72
|
+
function pgrst116(zeroRows: boolean): AuraError {
|
|
73
|
+
return new AuraError(406, {
|
|
74
|
+
error: {
|
|
75
|
+
code: 'PGRST116',
|
|
76
|
+
message: zeroRows
|
|
77
|
+
? 'JSON object requested, 0 rows returned'
|
|
78
|
+
: 'JSON object requested, multiple rows returned',
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// `id` non-null → mode COMPAT id-based (route `/{basePath}/{id}`, rétro-compat) ;
|
|
84
|
+
// `id: null` → mode FILTRÉ (route Table `/{basePath}?...` + filtres, parité PostgREST).
|
|
85
|
+
type MutationMode =
|
|
86
|
+
| { type: 'insert'; body: unknown }
|
|
87
|
+
| { type: 'update'; id: string | null; body: unknown }
|
|
88
|
+
| { type: 'delete'; id: string | null }
|
|
89
|
+
| { type: 'upsert'; body: unknown; onConflict: string }
|
|
90
|
+
|
|
91
|
+
/** État d'un builder en mode `rpc` (`DatabaseService.rpc()`) : args + verbe HTTP. */
|
|
92
|
+
export type RpcMode = { args: unknown; verb: 'POST' | 'GET' | 'HEAD' }
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Base class : état partagé, constructeur, exécution HTTP (`then`/`_execute`),
|
|
96
|
+
* `throwOnError` et `buildQueryString`. Implements PromiseLike so it can be
|
|
97
|
+
* awaited directly.
|
|
98
|
+
*/
|
|
99
|
+
export class AuraPostgrestBuilder<
|
|
100
|
+
// `ClientOptions`/`ThrowOnError` sont des paramètres fantômes : ils portent la
|
|
101
|
+
// signature (parité postgrest-js) et le threading des sous-classes, sans affecter
|
|
102
|
+
// le runtime ni la forme d'`AuraResponse`.
|
|
103
|
+
ClientOptions extends ClientServerOptions = AuraClientOptionsT,
|
|
104
|
+
Result = Record<string, unknown>,
|
|
105
|
+
ThrowOnError extends boolean = false,
|
|
106
|
+
> implements PromiseLike<AuraResponse<Result>>
|
|
107
|
+
{
|
|
108
|
+
protected readonly _client: AuraClientLike
|
|
109
|
+
protected readonly _basePath: string
|
|
110
|
+
protected _filters: StoredFilter[] = []
|
|
111
|
+
protected _orGroups: OrGroup[] = []
|
|
112
|
+
protected _selectColumns: string[] | null = null
|
|
113
|
+
// `select()` a-t-il été appelé ? (distinct de `_selectColumns`, que `select('*')`
|
|
114
|
+
// remet à `null`) — sert à poser `Prefer: return=representation` sur une mutation
|
|
115
|
+
// même avec `select('*')`.
|
|
116
|
+
protected _selectInvoked = false
|
|
117
|
+
protected _sortColumn: string | null = null
|
|
118
|
+
protected _sortDirection: 'asc' | 'desc' = 'asc'
|
|
119
|
+
protected _limit: number | null = null
|
|
120
|
+
protected _offset: number | null = null
|
|
121
|
+
protected _cursor: string | null = null
|
|
122
|
+
protected _count: CountMode | null = null
|
|
123
|
+
protected _rawSelect = false
|
|
124
|
+
protected _single = false
|
|
125
|
+
protected _maybeSingle = false
|
|
126
|
+
protected _throwOnError = false
|
|
127
|
+
protected _mutation: MutationMode | null = null
|
|
128
|
+
protected _rpc: RpcMode | null = null
|
|
129
|
+
|
|
130
|
+
constructor(client: AuraClientLike, basePath: string) {
|
|
131
|
+
this._client = client
|
|
132
|
+
this._basePath = basePath
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throwOnError(): this {
|
|
136
|
+
this._throwOnError = true
|
|
137
|
+
return this
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Positionne l'état interne `rpc` (args + verbe HTTP) — méthode package-interne
|
|
142
|
+
* utilisée par `DatabaseService.rpc()` pour éviter un cast sur un champ `protected`.
|
|
143
|
+
*
|
|
144
|
+
* `count` accepte `'planned'` (parité stricte `RpcOptions`, cf. `DatabaseService.rpc`)
|
|
145
|
+
* en plus de `CountMode` (`@aurabase/core`, qui ne connaît que `exact`/`estimated` —
|
|
146
|
+
* package partagé, non modifié ici). Le cast vers `_count` reste sans risque runtime :
|
|
147
|
+
* `buildQueryString` sérialise la valeur telle quelle en query param `count=`.
|
|
148
|
+
*/
|
|
149
|
+
_initRpc(args: unknown, verb: 'POST' | 'GET' | 'HEAD', count?: 'exact' | 'planned' | 'estimated'): this {
|
|
150
|
+
this._rpc = { args, verb }
|
|
151
|
+
if (count) this._count = count as CountMode
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// -- Query string builder ---------------------------------------------------
|
|
156
|
+
|
|
157
|
+
buildQueryString(): string {
|
|
158
|
+
const params = new URLSearchParams()
|
|
159
|
+
|
|
160
|
+
// Select (raw string preserves embed syntax with parentheses)
|
|
161
|
+
if (this._selectColumns && this._selectColumns.length > 0) {
|
|
162
|
+
params.set('select', this._rawSelect ? this._selectColumns[0]! : this._selectColumns.join(','))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Filters: col=op.value (encodage centralisé)
|
|
166
|
+
for (const f of this._filters) {
|
|
167
|
+
params.append(f.column, encodeFilterValue(f.op, f.value))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// OR groups — PostgREST syntax: or=(col.op.val,col2.op.val2)
|
|
171
|
+
for (const group of this._orGroups) {
|
|
172
|
+
const parts = group.filters.map((f) => `${f.column}.${encodeFilterValue(f.op, f.value)}`)
|
|
173
|
+
params.append('or', `(${parts.join(',')})`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Sort
|
|
177
|
+
if (this._sortColumn) {
|
|
178
|
+
params.set('sort_by', this._sortColumn)
|
|
179
|
+
params.set('sort_order', this._sortDirection)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Pagination
|
|
183
|
+
if (this._limit !== null) params.set('limit', String(this._limit))
|
|
184
|
+
if (this._offset !== null) params.set('offset', String(this._offset))
|
|
185
|
+
if (this._cursor !== null) params.set('cursor', this._cursor)
|
|
186
|
+
|
|
187
|
+
// Count
|
|
188
|
+
if (this._count) params.set('count', this._count)
|
|
189
|
+
|
|
190
|
+
// Args RPC en query params quand le verbe est GET (get:true) ou HEAD (head:true)
|
|
191
|
+
// — parité PostgREST/postgrest-js : args en query pour les deux verbes sans corps.
|
|
192
|
+
if (
|
|
193
|
+
this._rpc &&
|
|
194
|
+
(this._rpc.verb === 'GET' || this._rpc.verb === 'HEAD') &&
|
|
195
|
+
this._rpc.args &&
|
|
196
|
+
typeof this._rpc.args === 'object'
|
|
197
|
+
) {
|
|
198
|
+
for (const [k, v] of Object.entries(this._rpc.args as Record<string, unknown>)) {
|
|
199
|
+
params.set(k, typeof v === 'string' ? v : JSON.stringify(v))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return params.toString()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// -- Execution via PromiseLike ----------------------------------------------
|
|
207
|
+
|
|
208
|
+
then<TResult1 = AuraResponse<Result>, TResult2 = never>(
|
|
209
|
+
onfulfilled?: ((value: AuraResponse<Result>) => TResult1 | PromiseLike<TResult1>) | null,
|
|
210
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
211
|
+
): Promise<TResult1 | TResult2> {
|
|
212
|
+
return this._execute().then(onfulfilled, onrejected)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async _execute(): Promise<AuraResponse<Result>> {
|
|
216
|
+
let res: AuraResponse<Result>
|
|
217
|
+
|
|
218
|
+
// Parité postgrest-js : `single()`/`maybeSingle()` demandent un objet unique via l'en-tête
|
|
219
|
+
// Accept (et NON plus `limit=2`, qui casse sur les mutations filtrées). Fusionné avec les
|
|
220
|
+
// autres en-têtes (Prefer) dans chaque branche ci-dessous.
|
|
221
|
+
const acceptSingle: Record<string, string> | undefined = this._single
|
|
222
|
+
? { Accept: PGRST_OBJECT_ACCEPT }
|
|
223
|
+
: undefined
|
|
224
|
+
|
|
225
|
+
if (this._rpc) {
|
|
226
|
+
const qs = this.buildQueryString()
|
|
227
|
+
const sep = qs ? '?' : ''
|
|
228
|
+
const url = `${this._client.baseUrl}${this._basePath}${sep}${qs}`
|
|
229
|
+
const init: { method: string; body?: unknown; headers?: Record<string, string> } =
|
|
230
|
+
this._rpc.verb === 'POST'
|
|
231
|
+
? { method: 'POST', body: this._rpc.args, headers: acceptSingle }
|
|
232
|
+
: { method: this._rpc.verb, headers: acceptSingle }
|
|
233
|
+
res = await fetchWithAuth<Result>(url, init, this._client)
|
|
234
|
+
} else if (this._mutation) {
|
|
235
|
+
const m = this._mutation
|
|
236
|
+
|
|
237
|
+
// GARDE-FOU ANTI-WIPE : un update()/delete() en mode filtré SANS aucun
|
|
238
|
+
// filtre effacerait/modifierait toute la table (le gateway ne l'empêche
|
|
239
|
+
// pas). On refuse AVANT tout appel réseau. Le mode compat id-based
|
|
240
|
+
// (`m.id !== null`) porte implicitement un critère → non concerné.
|
|
241
|
+
//
|
|
242
|
+
// DB-F2 : on compte les filtres EFFECTIFS, pas la seule présence d'entrées.
|
|
243
|
+
// `.or([])` pousse un OrGroup vide (`{filters:[]}`) et `.filter('','','')`
|
|
244
|
+
// pousse un filtre à colonne vide — aucun des deux n'émet de critère
|
|
245
|
+
// PostgREST. Les ignorer ferme le contournement du garde-fou.
|
|
246
|
+
const hasEffectiveFilter =
|
|
247
|
+
this._filters.some((f) => f.column !== '') ||
|
|
248
|
+
this._orGroups.some((g) => g.filters.length > 0)
|
|
249
|
+
if (
|
|
250
|
+
(m.type === 'update' || m.type === 'delete') &&
|
|
251
|
+
m.id === null &&
|
|
252
|
+
!hasEffectiveFilter
|
|
253
|
+
) {
|
|
254
|
+
return {
|
|
255
|
+
data: null,
|
|
256
|
+
error: new AuraError(0, {
|
|
257
|
+
error: {
|
|
258
|
+
code: 'unfiltered_mutation',
|
|
259
|
+
message:
|
|
260
|
+
'update()/delete() sans filtre refusé (protection anti-wipe) — ajoutez un filtre .eq()/... ou utilisez une mutation id-based',
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// En-tête Prefer : `return=representation` si `.select()` (indispensable
|
|
267
|
+
// pour récupérer les lignes sur un DELETE ; inoffensif sur PATCH/POST) ;
|
|
268
|
+
// `count=<mode>` si `.count()` (parité PostgREST — sur mutation le comptage
|
|
269
|
+
// passe par Prefer, pas par le query param).
|
|
270
|
+
const prefer: string[] = []
|
|
271
|
+
if (this._selectInvoked) prefer.push('return=representation')
|
|
272
|
+
if (this._count) prefer.push(`count=${this._count}`)
|
|
273
|
+
// Fusion Prefer (mutation) + Accept object+json (single) — `.eq().update().select().single()`.
|
|
274
|
+
const merged: Record<string, string> = { ...acceptSingle }
|
|
275
|
+
if (prefer.length > 0) merged.Prefer = prefer.join(', ')
|
|
276
|
+
const headers = Object.keys(merged).length > 0 ? merged : undefined
|
|
277
|
+
|
|
278
|
+
// Query string commune (select + filtres) via le builder partagé.
|
|
279
|
+
const qs = this.buildQueryString()
|
|
280
|
+
const sep = qs ? '?' : ''
|
|
281
|
+
|
|
282
|
+
switch (m.type) {
|
|
283
|
+
case 'insert': {
|
|
284
|
+
res = await fetchWithAuth<Result>(
|
|
285
|
+
`${this._client.baseUrl}${this._basePath}${sep}${qs}`,
|
|
286
|
+
{ method: 'POST', body: m.body, headers },
|
|
287
|
+
this._client,
|
|
288
|
+
)
|
|
289
|
+
break
|
|
290
|
+
}
|
|
291
|
+
case 'update': {
|
|
292
|
+
// Compat : route id-based `/{basePath}/{id}` (rétro-compat exacte) ;
|
|
293
|
+
// sinon route Table filtrée `/{basePath}?filtres[&select]`.
|
|
294
|
+
const path = m.id !== null ? `${this._basePath}/${m.id}` : this._basePath
|
|
295
|
+
res = await fetchWithAuth<Result>(
|
|
296
|
+
`${this._client.baseUrl}${path}${sep}${qs}`,
|
|
297
|
+
{ method: 'PATCH', body: m.body, headers },
|
|
298
|
+
this._client,
|
|
299
|
+
)
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
case 'delete': {
|
|
303
|
+
const path = m.id !== null ? `${this._basePath}/${m.id}` : this._basePath
|
|
304
|
+
res = await fetchWithAuth<Result>(
|
|
305
|
+
`${this._client.baseUrl}${path}${sep}${qs}`,
|
|
306
|
+
{ method: 'DELETE', headers },
|
|
307
|
+
this._client,
|
|
308
|
+
)
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
case 'upsert': {
|
|
312
|
+
// `on_conflict` fusionné avec les éventuels filtres/select.
|
|
313
|
+
const params = new URLSearchParams(qs)
|
|
314
|
+
params.set('on_conflict', m.onConflict)
|
|
315
|
+
res = await fetchWithAuth<Result>(
|
|
316
|
+
`${this._client.baseUrl}${this._basePath}?${params.toString()}`,
|
|
317
|
+
{ method: 'POST', body: m.body, headers },
|
|
318
|
+
this._client,
|
|
319
|
+
)
|
|
320
|
+
break
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
// SELECT
|
|
325
|
+
const qs = this.buildQueryString()
|
|
326
|
+
const sep = qs ? '?' : ''
|
|
327
|
+
res = await fetchWithAuth<Result>(
|
|
328
|
+
`${this._client.baseUrl}${this._basePath}${sep}${qs}`,
|
|
329
|
+
{ method: 'GET', headers: acceptSingle },
|
|
330
|
+
this._client,
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// DB-F3 : parité postgrest-js — `insert()`/`upsert()` sans `.single()` promettent
|
|
335
|
+
// TOUJOURS un tableau (`Row[]`). Le gateway déballe le 201 mono-ligne en objet
|
|
336
|
+
// (postgrest_proxy : branche `is_created`, arr.len()==1 → unwrap). On re-emballe
|
|
337
|
+
// en tableau à 1 élément pour honorer le type de retour. N'affecte ni les inserts
|
|
338
|
+
// multi-lignes (déjà un tableau), ni `.single()` (bloc ci-dessous), ni un 204
|
|
339
|
+
// sans représentation (`res.data === null`).
|
|
340
|
+
if (
|
|
341
|
+
!this._single &&
|
|
342
|
+
(this._mutation?.type === 'insert' || this._mutation?.type === 'upsert') &&
|
|
343
|
+
res.data !== null &&
|
|
344
|
+
typeof res.data === 'object' &&
|
|
345
|
+
!Array.isArray(res.data)
|
|
346
|
+
) {
|
|
347
|
+
res = { ...res, data: [res.data] as unknown as Result }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Single / maybeSingle : cardinalité stricte (parité PostgREST/Supabase).
|
|
351
|
+
//
|
|
352
|
+
// Chemin nominal (backend honore Accept object+json) :
|
|
353
|
+
// - 1 ligne → `res.data` est déjà l'objet (pas un tableau) → renvoyé tel quel.
|
|
354
|
+
// - 0 / >1 → 406 (gateway : code `NOT_SINGULAR`, détail PostgREST « N rows »).
|
|
355
|
+
// • single → erreur PGRST116 (exige exactement 1 ligne).
|
|
356
|
+
// • maybeSingle → 0 ligne = `{data:null,error:null}` ; >1 = erreur PGRST116.
|
|
357
|
+
// Chemin défensif : si le backend renvoie malgré tout un TABLEAU (en-tête ignoré,
|
|
358
|
+
// mock de test…), on réduit avec la même sémantique.
|
|
359
|
+
if (this._single) {
|
|
360
|
+
if (res.error) {
|
|
361
|
+
// Cardinalité 0-ligne, selon la route empruntée par la mutation/lecture :
|
|
362
|
+
// - 406 NOT_SINGULAR (route Table/filtrée) : discrimine 0 vs >1 via le
|
|
363
|
+
// détail PostgREST (« N rows »).
|
|
364
|
+
// - 404 RECORD_NOT_FOUND (DB-F1) : le gateway réécrit PGRST116 en 404 sur
|
|
365
|
+
// la route id-based `/{table}/{id}` (postgrest_proxy `is_single_object`).
|
|
366
|
+
// C'est TOUJOURS un cas 0-ligne (l'id n'existe pas) — jamais « >1 ».
|
|
367
|
+
// `zero === null` → autre erreur (401/500…), propagée telle quelle.
|
|
368
|
+
const zero =
|
|
369
|
+
res.error.status === 406
|
|
370
|
+
? mentionsZeroRows(res.error)
|
|
371
|
+
: res.error.status === 404 && res.error.code === 'RECORD_NOT_FOUND'
|
|
372
|
+
? true
|
|
373
|
+
: null
|
|
374
|
+
if (zero !== null) {
|
|
375
|
+
res =
|
|
376
|
+
this._maybeSingle && zero
|
|
377
|
+
? { ...res, data: null, error: null }
|
|
378
|
+
: { ...res, data: null, error: pgrst116(zero) }
|
|
379
|
+
}
|
|
380
|
+
} else if (Array.isArray(res.data)) {
|
|
381
|
+
const rows = res.data as unknown[]
|
|
382
|
+
if (this._maybeSingle) {
|
|
383
|
+
res =
|
|
384
|
+
rows.length > 1
|
|
385
|
+
? { ...res, data: null, error: pgrst116(false) }
|
|
386
|
+
: { ...res, data: (rows[0] as Result | undefined) ?? null }
|
|
387
|
+
} else if (rows.length === 1) {
|
|
388
|
+
res = { ...res, data: rows[0] as Result }
|
|
389
|
+
} else {
|
|
390
|
+
res = { ...res, data: null, error: pgrst116(rows.length === 0) }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Sinon : `res.data` est déjà l'objet unique → renvoyé tel quel.
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Throw mode
|
|
397
|
+
if (this._throwOnError && res.error) {
|
|
398
|
+
const err = new Error(res.error.message)
|
|
399
|
+
;(err as unknown as Record<string, unknown>).code = res.error.code
|
|
400
|
+
;(err as unknown as Record<string, unknown>).details = res.error.details
|
|
401
|
+
throw err
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return res
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Sélection de colonnes, tri, pagination et modificateurs de cardinalité
|
|
410
|
+
* (`single`/`maybeSingle`/`count`).
|
|
411
|
+
*/
|
|
412
|
+
export class AuraTransformBuilder<
|
|
413
|
+
Schema extends GenericSchema = GenericSchema,
|
|
414
|
+
Row extends Record<string, unknown> = Record<string, unknown>,
|
|
415
|
+
RelationName = unknown,
|
|
416
|
+
Relationships = unknown,
|
|
417
|
+
Result = Record<string, unknown>,
|
|
418
|
+
ClientOptions extends ClientServerOptions = AuraClientOptionsT,
|
|
419
|
+
ThrowOnError extends boolean = false,
|
|
420
|
+
> extends AuraPostgrestBuilder<ClientOptions, Result, ThrowOnError> {
|
|
421
|
+
// -- Column selection & resource embedding -----------------------------------
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Select columns and optionally embed related resources (PostgREST parity).
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* // Simple columns
|
|
428
|
+
* db.from('users').select('id, name, email')
|
|
429
|
+
*
|
|
430
|
+
* // All columns (default)
|
|
431
|
+
* db.from('users').select('*')
|
|
432
|
+
*
|
|
433
|
+
* // Embed related table (many-to-one)
|
|
434
|
+
* db.from('orders').select('*, customer:customers(name, email)')
|
|
435
|
+
*
|
|
436
|
+
* // Embed related table (one-to-many)
|
|
437
|
+
* db.from('directors').select('last_name, films(title, year)')
|
|
438
|
+
*
|
|
439
|
+
* // FK disambiguation
|
|
440
|
+
* db.from('orders').select('*, billing:addresses!billing_address_id(name)')
|
|
441
|
+
*
|
|
442
|
+
* // Nested embedding
|
|
443
|
+
* db.from('actors').select('first_name, roles(character, films(title))')
|
|
444
|
+
*/
|
|
445
|
+
select<
|
|
446
|
+
Query extends string = '*',
|
|
447
|
+
NewResultOne = GetResult<Schema, Row, RelationName, Relationships, Query, ClientOptions>,
|
|
448
|
+
>(
|
|
449
|
+
columns?: Query,
|
|
450
|
+
): AuraFilterBuilder<
|
|
451
|
+
Schema,
|
|
452
|
+
Row,
|
|
453
|
+
RelationName,
|
|
454
|
+
Relationships,
|
|
455
|
+
NewResultOne[],
|
|
456
|
+
ClientOptions,
|
|
457
|
+
ThrowOnError
|
|
458
|
+
> {
|
|
459
|
+
// Runtime inchangé : accepte aussi un tableau de colonnes (parité historique),
|
|
460
|
+
// le typage n'expose que la forme string literal pour l'inférence PostgREST.
|
|
461
|
+
this._selectInvoked = true
|
|
462
|
+
const cols = columns as unknown as string | string[] | undefined
|
|
463
|
+
if (cols === undefined || cols === '*') {
|
|
464
|
+
this._selectColumns = null
|
|
465
|
+
this._rawSelect = false
|
|
466
|
+
} else if (typeof cols === 'string') {
|
|
467
|
+
// If string contains parentheses → embed syntax, pass raw to backend
|
|
468
|
+
// Otherwise → simple comma-separated column list
|
|
469
|
+
if (cols.includes('(')) {
|
|
470
|
+
this._selectColumns = [cols]
|
|
471
|
+
this._rawSelect = true
|
|
472
|
+
} else {
|
|
473
|
+
this._selectColumns = cols.split(',').map((c) => c.trim())
|
|
474
|
+
this._rawSelect = false
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
this._selectColumns = cols
|
|
478
|
+
this._rawSelect = false
|
|
479
|
+
}
|
|
480
|
+
return this as unknown as AuraFilterBuilder<
|
|
481
|
+
Schema,
|
|
482
|
+
Row,
|
|
483
|
+
RelationName,
|
|
484
|
+
Relationships,
|
|
485
|
+
NewResultOne[],
|
|
486
|
+
ClientOptions,
|
|
487
|
+
ThrowOnError
|
|
488
|
+
>
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// -- Sort / pagination ------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
order(column: string, direction: 'asc' | 'desc' = 'asc'): this {
|
|
494
|
+
this._sortColumn = column
|
|
495
|
+
this._sortDirection = direction
|
|
496
|
+
return this
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
limit(n: number): this {
|
|
500
|
+
this._limit = n
|
|
501
|
+
return this
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
offset(n: number): this {
|
|
505
|
+
this._offset = n
|
|
506
|
+
return this
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Pagination keyset : passer le `next_cursor` opaque de la page précédente (préférable à
|
|
511
|
+
* `offset` sur les grandes tables : stable, sans dup/skip). Renvoyer tel quel `meta.next_cursor` ;
|
|
512
|
+
* ne pas le construire/parser (format opaque).
|
|
513
|
+
*/
|
|
514
|
+
cursor(cursor: string): this {
|
|
515
|
+
this._cursor = cursor
|
|
516
|
+
return this
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
count(mode: CountMode = 'exact'): this {
|
|
520
|
+
this._count = mode
|
|
521
|
+
return this
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// -- Modifiers --------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
single<ResultOne = Result extends (infer U)[] ? U : Result>(): AuraPostgrestBuilder<
|
|
527
|
+
ClientOptions,
|
|
528
|
+
ResultOne,
|
|
529
|
+
ThrowOnError
|
|
530
|
+
> {
|
|
531
|
+
this._single = true
|
|
532
|
+
// Parité postgrest-js : `_execute` envoie `Accept: application/vnd.pgrst.object+json`.
|
|
533
|
+
// PostgREST renvoie alors l'objet unique (200) ou 406 PGRST116 (0/>1 lignes). On
|
|
534
|
+
// n'ajoute PLUS `limit=2` : PostgREST rejette `limit` sans `order` sur une mutation
|
|
535
|
+
// filtrée (`.eq().update().select().single()` cassait). L'en-tête traverse le gateway
|
|
536
|
+
// (route Table) jusqu'à PostgREST — cf. postgrest_proxy `req.into_parts()`.
|
|
537
|
+
return this as unknown as AuraPostgrestBuilder<ClientOptions, ResultOne, ThrowOnError>
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
maybeSingle<ResultOne = Result extends (infer U)[] ? U : Result>(): AuraPostgrestBuilder<
|
|
541
|
+
ClientOptions,
|
|
542
|
+
ResultOne | null,
|
|
543
|
+
ThrowOnError
|
|
544
|
+
> {
|
|
545
|
+
this._single = true
|
|
546
|
+
this._maybeSingle = true
|
|
547
|
+
// Même en-tête `Accept: object+json`. Différence : sur 406 « 0 rows » → `{data:null,error:null}`
|
|
548
|
+
// (0 ligne n'est pas une erreur pour maybeSingle) ; sur >1 ligne → erreur PGRST116.
|
|
549
|
+
return this as unknown as AuraPostgrestBuilder<ClientOptions, ResultOne | null, ThrowOnError>
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Tous les filtres PostgREST (`eq`, `neq`, ... `ilikeAnyOf`).
|
|
555
|
+
*/
|
|
556
|
+
export class AuraFilterBuilder<
|
|
557
|
+
Schema extends GenericSchema = GenericSchema,
|
|
558
|
+
Row extends Record<string, unknown> = Record<string, unknown>,
|
|
559
|
+
RelationName = unknown,
|
|
560
|
+
Relationships = unknown,
|
|
561
|
+
Result = Record<string, unknown>,
|
|
562
|
+
ClientOptions extends ClientServerOptions = AuraClientOptionsT,
|
|
563
|
+
ThrowOnError extends boolean = false,
|
|
564
|
+
> extends AuraTransformBuilder<
|
|
565
|
+
Schema,
|
|
566
|
+
Row,
|
|
567
|
+
RelationName,
|
|
568
|
+
Relationships,
|
|
569
|
+
Result,
|
|
570
|
+
ClientOptions,
|
|
571
|
+
ThrowOnError
|
|
572
|
+
> {
|
|
573
|
+
// -- Filter methods (each returns this for chaining) ------------------------
|
|
574
|
+
//
|
|
575
|
+
// Typées sur `Row` : `column` est contraint à `keyof Row & string` (autocomplete +
|
|
576
|
+
// erreur de compilation sur colonne inconnue) et `value`/`values` à `Row[C]` (erreur
|
|
577
|
+
// sur mauvais type). Runtime inchangé — seules les signatures changent.
|
|
578
|
+
|
|
579
|
+
eq<C extends keyof Row & string>(column: C, value: Row[C]): this {
|
|
580
|
+
this._filters.push({ column, op: 'eq', value })
|
|
581
|
+
return this
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
neq<C extends keyof Row & string>(column: C, value: Row[C]): this {
|
|
585
|
+
this._filters.push({ column, op: 'neq', value })
|
|
586
|
+
return this
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
gt<C extends keyof Row & string>(column: C, value: Row[C]): this {
|
|
590
|
+
this._filters.push({ column, op: 'gt', value })
|
|
591
|
+
return this
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
gte<C extends keyof Row & string>(column: C, value: Row[C]): this {
|
|
595
|
+
this._filters.push({ column, op: 'gte', value })
|
|
596
|
+
return this
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
lt<C extends keyof Row & string>(column: C, value: Row[C]): this {
|
|
600
|
+
this._filters.push({ column, op: 'lt', value })
|
|
601
|
+
return this
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
lte<C extends keyof Row & string>(column: C, value: Row[C]): this {
|
|
605
|
+
this._filters.push({ column, op: 'lte', value })
|
|
606
|
+
return this
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
like(column: keyof Row & string, pattern: string): this {
|
|
610
|
+
this._filters.push({ column, op: 'like', value: pattern })
|
|
611
|
+
return this
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
ilike(column: keyof Row & string, pattern: string): this {
|
|
615
|
+
this._filters.push({ column, op: 'ilike', value: pattern })
|
|
616
|
+
return this
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* IS NULL or IS NOT NULL filter.
|
|
621
|
+
* PostgREST accepts: is.null, is.true, is.false (no "not_null" — use neq for that).
|
|
622
|
+
* For IS NOT NULL, use `neq(column, null)` or `is(column, 'not.null')`.
|
|
623
|
+
*/
|
|
624
|
+
is(column: keyof Row & string, value: 'null' | 'true' | 'false' | 'unknown' | 'not.null'): this {
|
|
625
|
+
if (value === 'not.null') {
|
|
626
|
+
// IS NOT NULL en PostgREST = `col=not.is.null`. (`neq.null` produit `col <> NULL`,
|
|
627
|
+
// toujours NULL/faux en SQL → renverrait 0 ligne.)
|
|
628
|
+
this._filters.push({ column, op: 'not.is', value: 'null' })
|
|
629
|
+
} else {
|
|
630
|
+
this._filters.push({ column, op: 'is', value })
|
|
631
|
+
}
|
|
632
|
+
return this
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
in<C extends keyof Row & string>(column: C, values: Row[C][]): this {
|
|
636
|
+
this._filters.push({ column, op: 'in', value: values })
|
|
637
|
+
return this
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Add an OR group: `or=(col1.eq.val1,col2.gt.val2)`
|
|
642
|
+
*/
|
|
643
|
+
or(filters: Array<{ column: string; op: string; value: unknown }>): this {
|
|
644
|
+
this._orGroups.push({
|
|
645
|
+
filters: filters.map((f) => ({ column: f.column, op: f.op, value: f.value })),
|
|
646
|
+
})
|
|
647
|
+
return this
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// -- Filtres étendus (parité PostgREST) -------------------------------------
|
|
651
|
+
|
|
652
|
+
notIn<C extends keyof Row & string>(column: C, values: Row[C][]): this { this._filters.push({ column, op: 'not.in', value: values }); return this }
|
|
653
|
+
contains(column: keyof Row & string, value: unknown): this { this._filters.push({ column, op: 'cs', value }); return this }
|
|
654
|
+
containedBy(column: keyof Row & string, value: unknown): this { this._filters.push({ column, op: 'cd', value }); return this }
|
|
655
|
+
overlaps(column: keyof Row & string, value: unknown): this { this._filters.push({ column, op: 'ov', value }); return this }
|
|
656
|
+
rangeGt(column: keyof Row & string, range: string): this { this._filters.push({ column, op: 'sr', value: range }); return this }
|
|
657
|
+
rangeGte(column: keyof Row & string, range: string): this { this._filters.push({ column, op: 'nxl', value: range }); return this }
|
|
658
|
+
rangeLt(column: keyof Row & string, range: string): this { this._filters.push({ column, op: 'sl', value: range }); return this }
|
|
659
|
+
rangeLte(column: keyof Row & string, range: string): this { this._filters.push({ column, op: 'nxr', value: range }); return this }
|
|
660
|
+
rangeAdjacent(column: keyof Row & string, range: string): this { this._filters.push({ column, op: 'adj', value: range }); return this }
|
|
661
|
+
isDistinct(column: keyof Row & string, value: unknown): this { this._filters.push({ column, op: 'isdistinct', value }); return this }
|
|
662
|
+
// Échappatoires (parité postgrest-js) : colonne `string` permissive, pas de contrainte sur `Row`.
|
|
663
|
+
not(column: string, op: string, value: unknown): this { this._filters.push({ column, op: `not.${op}`, value }); return this }
|
|
664
|
+
filter(column: string, op: string, value: unknown): this { this._filters.push({ column, op, value }); return this }
|
|
665
|
+
match(query: Record<string, unknown>): this {
|
|
666
|
+
for (const [column, value] of Object.entries(query)) this._filters.push({ column, op: 'eq', value })
|
|
667
|
+
return this
|
|
668
|
+
}
|
|
669
|
+
textSearch(column: keyof Row & string, query: string, options?: { type?: 'plain' | 'phrase' | 'websearch'; config?: string }): this {
|
|
670
|
+
const t = options?.type
|
|
671
|
+
const token = t === 'plain' ? 'plfts' : t === 'phrase' ? 'phfts' : t === 'websearch' ? 'wfts' : 'fts'
|
|
672
|
+
const op = options?.config ? `${token}(${options.config})` : token
|
|
673
|
+
this._filters.push({ column, op, value: query })
|
|
674
|
+
return this
|
|
675
|
+
}
|
|
676
|
+
likeAllOf(column: keyof Row & string, patterns: string[]): this { this._filters.push({ column, op: 'like(all)', value: patterns }); return this }
|
|
677
|
+
likeAnyOf(column: keyof Row & string, patterns: string[]): this { this._filters.push({ column, op: 'like(any)', value: patterns }); return this }
|
|
678
|
+
ilikeAllOf(column: keyof Row & string, patterns: string[]): this { this._filters.push({ column, op: 'ilike(all)', value: patterns }); return this }
|
|
679
|
+
ilikeAnyOf(column: keyof Row & string, patterns: string[]): this { this._filters.push({ column, op: 'ilike(any)', value: patterns }); return this }
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Chainable query builder that compiles to URL query parameters
|
|
684
|
+
* matching the backend's QueryOptions / FilterOp format.
|
|
685
|
+
*
|
|
686
|
+
* Point d'entrée concret retourné par `DatabaseService.from()` : ajoute les
|
|
687
|
+
* mutations (`insert`/`update`/`delete`/`upsert`) au-dessus de la chaîne
|
|
688
|
+
* Base → Transform → Filter.
|
|
689
|
+
*/
|
|
690
|
+
export class AuraQueryBuilder<
|
|
691
|
+
Schema extends GenericSchema = GenericSchema,
|
|
692
|
+
Relation extends GenericTable | GenericView = GenericTable,
|
|
693
|
+
RelationName = unknown,
|
|
694
|
+
ClientOptions extends ClientServerOptions = AuraClientOptionsT,
|
|
695
|
+
> extends AuraFilterBuilder<
|
|
696
|
+
Schema,
|
|
697
|
+
Relation['Row'],
|
|
698
|
+
RelationName,
|
|
699
|
+
Relation['Relationships'],
|
|
700
|
+
Relation['Row'][],
|
|
701
|
+
ClientOptions,
|
|
702
|
+
false
|
|
703
|
+
> {
|
|
704
|
+
// -- Mutations --------------------------------------------------------------
|
|
705
|
+
//
|
|
706
|
+
// Typées sur `Relation['Insert']`/`Relation['Update']` (parité postgrest-js) :
|
|
707
|
+
// un champ requis manquant côté insert/upsert, ou une colonne inconnue côté
|
|
708
|
+
// update, est une erreur de compilation. Runtime inchangé.
|
|
709
|
+
//
|
|
710
|
+
// Extraction par type conditionnel (plutôt qu'un accès indexé direct
|
|
711
|
+
// `Relation['Insert']`) : `Relation` est contraint à `GenericTable | GenericView`
|
|
712
|
+
// et `GenericNonUpdatableView` ne porte pas `Insert`/`Update`. `infer` distribue
|
|
713
|
+
// sur cette union et retombe sur `never` pour la branche non-updatable, ce qui
|
|
714
|
+
// évite l'erreur TS2536 tout en gardant le typage strict pour les tables.
|
|
715
|
+
|
|
716
|
+
insert(
|
|
717
|
+
body: Relation extends { Insert: infer I } ? I | I[] : never,
|
|
718
|
+
): this {
|
|
719
|
+
this._mutation = { type: 'insert', body }
|
|
720
|
+
return this
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// `update`/`delete`/`upsert` retournent un `AuraFilterBuilder` (au lieu de
|
|
724
|
+
// `this`) : le mode filtré chaîne `.eq(...)` AVANT et `.select()` APRÈS la
|
|
725
|
+
// mutation (parité PostgREST). Deux formes pour `update` :
|
|
726
|
+
// - COMPAT `update(id: string, body)` → route id-based `/{table}/{id}` ;
|
|
727
|
+
// - FILTRÉ `update(body)` → route Table `/{table}?filtres`, exige un filtre
|
|
728
|
+
// (garde-fou anti-wipe dans `_execute`).
|
|
729
|
+
update(
|
|
730
|
+
id: string,
|
|
731
|
+
body: Relation extends { Update: infer U } ? U : never,
|
|
732
|
+
): AuraFilterBuilder<
|
|
733
|
+
Schema,
|
|
734
|
+
Relation['Row'],
|
|
735
|
+
RelationName,
|
|
736
|
+
Relation['Relationships'],
|
|
737
|
+
Relation['Row'][],
|
|
738
|
+
ClientOptions,
|
|
739
|
+
false
|
|
740
|
+
>
|
|
741
|
+
update(
|
|
742
|
+
body: Relation extends { Update: infer U } ? U : never,
|
|
743
|
+
): AuraFilterBuilder<
|
|
744
|
+
Schema,
|
|
745
|
+
Relation['Row'],
|
|
746
|
+
RelationName,
|
|
747
|
+
Relation['Relationships'],
|
|
748
|
+
Relation['Row'][],
|
|
749
|
+
ClientOptions,
|
|
750
|
+
false
|
|
751
|
+
>
|
|
752
|
+
update(
|
|
753
|
+
idOrBody: string | (Relation extends { Update: infer U } ? U : never),
|
|
754
|
+
body?: Relation extends { Update: infer U } ? U : never,
|
|
755
|
+
): AuraFilterBuilder<
|
|
756
|
+
Schema,
|
|
757
|
+
Relation['Row'],
|
|
758
|
+
RelationName,
|
|
759
|
+
Relation['Relationships'],
|
|
760
|
+
Relation['Row'][],
|
|
761
|
+
ClientOptions,
|
|
762
|
+
false
|
|
763
|
+
> {
|
|
764
|
+
if (typeof idOrBody === 'string') {
|
|
765
|
+
this._mutation = { type: 'update', id: idOrBody, body }
|
|
766
|
+
} else {
|
|
767
|
+
this._mutation = { type: 'update', id: null, body: idOrBody }
|
|
768
|
+
}
|
|
769
|
+
return this as unknown as AuraFilterBuilder<
|
|
770
|
+
Schema,
|
|
771
|
+
Relation['Row'],
|
|
772
|
+
RelationName,
|
|
773
|
+
Relation['Relationships'],
|
|
774
|
+
Relation['Row'][],
|
|
775
|
+
ClientOptions,
|
|
776
|
+
false
|
|
777
|
+
>
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// COMPAT `delete(id)` → route id-based ; FILTRÉ `delete()` → route Table
|
|
781
|
+
// (exige un filtre, garde-fou anti-wipe).
|
|
782
|
+
delete(id?: string): AuraFilterBuilder<
|
|
783
|
+
Schema,
|
|
784
|
+
Relation['Row'],
|
|
785
|
+
RelationName,
|
|
786
|
+
Relation['Relationships'],
|
|
787
|
+
Relation['Row'][],
|
|
788
|
+
ClientOptions,
|
|
789
|
+
false
|
|
790
|
+
> {
|
|
791
|
+
this._mutation = { type: 'delete', id: typeof id === 'string' ? id : null }
|
|
792
|
+
return this as unknown as AuraFilterBuilder<
|
|
793
|
+
Schema,
|
|
794
|
+
Relation['Row'],
|
|
795
|
+
RelationName,
|
|
796
|
+
Relation['Relationships'],
|
|
797
|
+
Relation['Row'][],
|
|
798
|
+
ClientOptions,
|
|
799
|
+
false
|
|
800
|
+
>
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
upsert(
|
|
804
|
+
body: Relation extends { Insert: infer I } ? I | I[] : never,
|
|
805
|
+
options?: { onConflict?: string },
|
|
806
|
+
): AuraFilterBuilder<
|
|
807
|
+
Schema,
|
|
808
|
+
Relation['Row'],
|
|
809
|
+
RelationName,
|
|
810
|
+
Relation['Relationships'],
|
|
811
|
+
Relation['Row'][],
|
|
812
|
+
ClientOptions,
|
|
813
|
+
false
|
|
814
|
+
> {
|
|
815
|
+
this._mutation = { type: 'upsert', body, onConflict: options?.onConflict ?? 'id' }
|
|
816
|
+
return this as unknown as AuraFilterBuilder<
|
|
817
|
+
Schema,
|
|
818
|
+
Relation['Row'],
|
|
819
|
+
RelationName,
|
|
820
|
+
Relation['Relationships'],
|
|
821
|
+
Relation['Row'][],
|
|
822
|
+
ClientOptions,
|
|
823
|
+
false
|
|
824
|
+
>
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Alias rétro-compatible : le nom `QueryBuilder` reste l'export public utilisé
|
|
829
|
+
// par la façade et les tests existants.
|
|
830
|
+
export { AuraQueryBuilder as QueryBuilder }
|