Init
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
# Agent instructions for `@supabase/postgrest-js`
|
||||
|
||||
Isomorphic PostgREST client for database operations (select, insert, update, upsert, delete, filters, RPC).
|
||||
|
||||
When helping a user work with this package:
|
||||
|
||||
- **Usage and quick start**: [`README.md`](./README.md)
|
||||
- **Full API reference**: [`src/`](./src/) — every public method and type has TSDoc with `@example` blocks. Read the source for the canonical, version-pinned answer.
|
||||
- **Migration notes**: [`migrations/`](./migrations/) — per-theme markdown files for changes that need caller action.
|
||||
|
||||
For broader Supabase guidance (docs site, MCP tools, schema and project workflows), use the centralized [Supabase agent skills](https://github.com/supabase/agent-skills): `npx skills add supabase/agent-skills`. Or pair with the Supabase MCP server via the [Supabase Plugin for AI Coding Agents](https://supabase.com/docs/guides/getting-started/plugins).
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Supabase
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://supabase.io">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/supabase-logo-wordmark--dark.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/supabase-logo-wordmark--light.svg">
|
||||
<img alt="Supabase Logo" width="300" src="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/logo-preview.jpg">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
<h1 align="center">Supabase PostgREST JS SDK</h1>
|
||||
|
||||
<h3 align="center">Isomorphic JavaScript SDK for <a href="https://postgrest.org">PostgREST</a> with an ORM-like interface.</h3>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://supabase.com/docs/guides/database">Guides</a>
|
||||
·
|
||||
<a href="https://supabase.com/docs/reference/javascript/select">Reference Docs</a>
|
||||
·
|
||||
<a href="https://supabase.github.io/supabase-js/postgrest-js/v2/spec.json">TypeDoc</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/supabase/supabase-js/actions?query=branch%3Amaster)
|
||||
[](https://www.npmjs.com/package/@supabase/postgrest-js)
|
||||
[](#license)
|
||||
[](https://pkg.pr.new/~/supabase/postgrest-js)
|
||||
|
||||
</div>
|
||||
|
||||
### Quick start
|
||||
|
||||
Install
|
||||
|
||||
```bash
|
||||
npm install @supabase/postgrest-js
|
||||
```
|
||||
|
||||
Usage
|
||||
|
||||
```js
|
||||
import { PostgrestClient } from '@supabase/postgrest-js'
|
||||
|
||||
const REST_URL = 'http://localhost:3000'
|
||||
const postgrest = new PostgrestClient(REST_URL)
|
||||
```
|
||||
|
||||
- [select()](https://supabase.com/docs/reference/javascript/select)
|
||||
- [insert()](https://supabase.com/docs/reference/javascript/insert)
|
||||
- [update()](https://supabase.com/docs/reference/javascript/update)
|
||||
- [delete()](https://supabase.com/docs/reference/javascript/delete)
|
||||
|
||||
#### Custom `fetch` implementation
|
||||
|
||||
`postgrest-js` uses the runtime's global `fetch` to make HTTP requests, but an alternative `fetch` implementation can be provided as an option. This is useful in environments where the global `fetch` is unavailable or where you want to customize request behavior:
|
||||
|
||||
```js
|
||||
import { PostgrestClient } from '@supabase/postgrest-js'
|
||||
|
||||
const REST_URL = 'http://localhost:3000'
|
||||
const postgrest = new PostgrestClient(REST_URL, {
|
||||
fetch: (...args) => fetch(...args),
|
||||
})
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
This package is part of the [Supabase JavaScript monorepo](https://github.com/supabase/supabase-js). To work on this package:
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build (from monorepo root)
|
||||
pnpm nx build postgrest-js
|
||||
|
||||
# Build with watch mode for development
|
||||
pnpm nx build:watch postgrest-js
|
||||
|
||||
# TypeScript type checking
|
||||
pnpm nx type-check postgrest-js
|
||||
|
||||
# Generate documentation
|
||||
pnpm nx docs postgrest-js
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
**Supabase CLI Required!** The `postgrest-js` tests use the [Supabase CLI](https://supabase.com/docs/guides/local-development/cli/getting-started) to run a local PostgreSQL database and PostgREST server.
|
||||
|
||||
#### Quick Start
|
||||
|
||||
```bash
|
||||
# Run all tests (from monorepo root)
|
||||
pnpm nx test:ci:postgrest postgrest-js
|
||||
```
|
||||
|
||||
This single command automatically:
|
||||
|
||||
1. Stops any existing Supabase CLI containers
|
||||
2. Starts PostgreSQL database and PostgREST server via Supabase CLI
|
||||
3. Resets and seeds the database
|
||||
4. Runs all Jest unit tests with coverage
|
||||
5. Cleans up containers
|
||||
|
||||
#### Individual Test Commands
|
||||
|
||||
```bash
|
||||
# Run Jest tests with coverage (requires infrastructure running)
|
||||
pnpm nx test:run postgrest-js
|
||||
|
||||
# Run type tests with tstyche
|
||||
pnpm nx test:types postgrest-js
|
||||
|
||||
# Run smoke tests (CommonJS and ESM imports)
|
||||
pnpm nx test:smoke postgrest-js
|
||||
|
||||
# Format code
|
||||
pnpm nx format postgrest-js
|
||||
|
||||
# Check formatting
|
||||
pnpm nx format:check postgrest-js
|
||||
```
|
||||
|
||||
#### Test Infrastructure
|
||||
|
||||
The tests use Supabase CLI to spin up:
|
||||
|
||||
- **PostgreSQL** - Database with test schema and seed data (port 54322)
|
||||
- **PostgREST** - REST API server that the client connects to (port 54321)
|
||||
|
||||
```bash
|
||||
# Manually manage test infrastructure (from monorepo root)
|
||||
pnpm nx test:infra postgrest-js # Start containers
|
||||
pnpm nx test:clean-pre postgrest-js # Stop and remove containers
|
||||
```
|
||||
|
||||
Or directly via Supabase CLI:
|
||||
|
||||
```bash
|
||||
cd packages/core/postgrest-js
|
||||
npx supabase --workdir ./test start # Start all services
|
||||
npx supabase --workdir ./test db reset # Reset and seed database
|
||||
npx supabase --workdir ./test stop # Stop all services
|
||||
```
|
||||
|
||||
#### Regenerating TypeScript Types
|
||||
|
||||
When the database schema changes, regenerate TypeScript types from the actual database:
|
||||
|
||||
```bash
|
||||
# From the monorepo root
|
||||
pnpm run codegen:postgrest
|
||||
```
|
||||
|
||||
This command automatically:
|
||||
|
||||
1. Cleans up any existing Supabase containers
|
||||
2. Starts Supabase (PostgreSQL, PostgREST, and all services)
|
||||
3. Generates TypeScript types from the database schema
|
||||
4. Post-processes the generated types (updates JSON type definitions)
|
||||
5. Formats the generated file with Prettier
|
||||
6. Cleans up Supabase containers
|
||||
|
||||
The generated types are written to `test/types.generated.ts`.
|
||||
|
||||
#### Test Types Explained
|
||||
|
||||
- **Unit Tests** - Jest tests covering all client functionality (`pnpm nx test:run postgrest-js`)
|
||||
- **Type Tests** - Validates TypeScript types using tstyche (`pnpm nx test:types postgrest-js`)
|
||||
- **Smoke Tests** - Basic import/require tests for CommonJS and ESM (`pnpm nx test:smoke postgrest-js`)
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- **Supabase CLI** must be installed ([instructions](https://supabase.com/docs/guides/local-development/cli/getting-started)) or can be used through `npx` (`npx supabase`)
|
||||
- **Docker** must be installed and running (Supabase CLI uses Docker under the hood)
|
||||
- **Port 54321** - PostgREST API
|
||||
- **Port 54322** - PostgreSQL database
|
||||
- **Port 54323** - Supabase Studio (used for type generation)
|
||||
|
||||
#### PostgREST v12 Backward Compatibility Tests
|
||||
|
||||
We maintain backward compatibility tests for PostgREST v12 (the current Supabase CLI uses v14+). These tests ensure the SDK works correctly for users still running older PostgREST versions.
|
||||
|
||||
```bash
|
||||
# Run v12 compatibility tests (requires Docker)
|
||||
pnpm nx test:ci:v12 postgrest-js
|
||||
```
|
||||
|
||||
This command:
|
||||
|
||||
1. Starts PostgREST v12 + PostgreSQL in Docker (ports 3012/5433)
|
||||
2. Runs runtime tests that verify v12-specific behavior
|
||||
3. Cleans up containers
|
||||
|
||||
**Type-only tests** for v12 compatibility also run as part of the regular type tests:
|
||||
|
||||
```bash
|
||||
pnpm nx test:types postgrest-js # Includes v12-compat.test-d.ts
|
||||
```
|
||||
|
||||
**Note:** These v12 tests will be removed when v3 ships (sometime in 2026).
|
||||
|
||||
### Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](../../../CONTRIBUTING.md) for details on how to get started.
|
||||
|
||||
For major changes or if you're unsure about something, please open an issue first to discuss your proposed changes.
|
||||
|
||||
## License
|
||||
|
||||
This repo is licensed under MIT License.
|
||||
+3888
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+5276
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+5276
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+3880
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+25
@@ -0,0 +1,25 @@
|
||||
# Migration notes for `@supabase/postgrest-js`
|
||||
|
||||
Each file in this directory describes one migration theme — what changed in `@supabase/postgrest-js`, who is affected, and what callers need to do.
|
||||
|
||||
Files are shipped with the npm package, so the migration notes you see here are pinned to the version of `@supabase/postgrest-js` you have installed. Upgrading the package brings the relevant migration notes along with it.
|
||||
|
||||
## How agents should use this directory
|
||||
|
||||
When helping a developer upgrade `@supabase/postgrest-js`:
|
||||
|
||||
1. Read the migration files in `node_modules/@supabase/postgrest-js/migrations/` for the version they have installed.
|
||||
2. Cross-reference against the version they are upgrading to.
|
||||
3. Apply the migration steps described in each relevant file.
|
||||
|
||||
## How humans should use this directory
|
||||
|
||||
Browse the files for the migration theme you care about. Each file is self-contained and explains its own scope, audience, and steps.
|
||||
|
||||
## File naming
|
||||
|
||||
One file per migration theme, named by topic rather than version (e.g. `<theme>.md`, kebab-case). A single version can ship multiple theme files; a single theme can span multiple versions. Each file documents its own `Since` / `Will require action by` version range internally.
|
||||
|
||||
## Cross-cutting migration notes
|
||||
|
||||
Migrations that span multiple Supabase packages (e.g. Node.js version drops, monorepo restructures) live in [`docs/MIGRATION.md`](../../../../docs/MIGRATION.md) at the repository root, not here. This directory is scoped to `@supabase/postgrest-js` only.
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "@supabase/postgrest-js",
|
||||
"version": "2.110.8",
|
||||
"description": "Isomorphic PostgREST client",
|
||||
"keywords": [
|
||||
"postgrest",
|
||||
"supabase"
|
||||
],
|
||||
"homepage": "https://github.com/supabase/supabase-js/tree/master/packages/core/postgrest-js",
|
||||
"bugs": "https://github.com/supabase/supabase-js/issues",
|
||||
"license": "MIT",
|
||||
"author": "Supabase",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"migrations",
|
||||
"AGENTS.md"
|
||||
],
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.cts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./dist/*": "./dist/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/supabase/supabase-js.git",
|
||||
"directory": "packages/core/postgrest-js"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "^30.0.0",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/node": "20.19.9",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"jest": "30.4.2",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.4.9",
|
||||
"tsdown": "^0.18.0",
|
||||
"tstyche": "^4.3.0",
|
||||
"type-fest": "^4.32.0",
|
||||
"typedoc": "^0.27.9",
|
||||
"typescript": "~5.8.3",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"build:watch": "tsdown --watch",
|
||||
"format": "node scripts/format.js",
|
||||
"format:check": "node scripts/format.js check",
|
||||
"docs": "typedoc --options ../../../typedoc.base.mjs src/index.ts --out docs/v2",
|
||||
"docs:json": "typedoc --options ../../../typedoc.base.mjs --json docs/v2/spec.json --excludeExternals src/index.ts",
|
||||
"test:run": "jest --runInBand --coverage -u",
|
||||
"test:smoke": "node test/smoke.cjs && node test/smoke.mjs",
|
||||
"test:types": "tstyche",
|
||||
"test:types:ci": "tstyche --target '4.7,5.5,6.0'",
|
||||
"test:types:watch": "chokidar 'src/**/*.ts' 'test/**/*.ts' -c 'npm run test:types'",
|
||||
"type-check": "tsc --noEmit --project tsconfig.json",
|
||||
"type-check:test": "tsc --noEmit --project tsconfig.test.json"
|
||||
}
|
||||
}
|
||||
+718
@@ -0,0 +1,718 @@
|
||||
import type {
|
||||
PostgrestSingleResponse,
|
||||
PostgrestResponseSuccess,
|
||||
CheckMatchingArrayTypes,
|
||||
MergePartialResult,
|
||||
IsValidResultOverride,
|
||||
} from './types/types'
|
||||
import {
|
||||
ClientServerOptions,
|
||||
Fetch,
|
||||
DEFAULT_MAX_RETRIES,
|
||||
getRetryDelay,
|
||||
RETRYABLE_STATUS_CODES,
|
||||
RETRYABLE_METHODS,
|
||||
} from './types/common/common'
|
||||
import PostgrestError from './PostgrestError'
|
||||
import { ContainsNull } from './select-query-parser/types'
|
||||
|
||||
/**
|
||||
* Sleep for a given number of milliseconds.
|
||||
* If an AbortSignal is provided, the sleep resolves early when the signal is aborted.
|
||||
*/
|
||||
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (signal?.aborted) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const id = setTimeout(() => {
|
||||
signal?.removeEventListener('abort', onAbort)
|
||||
resolve()
|
||||
}, ms)
|
||||
function onAbort() {
|
||||
clearTimeout(id)
|
||||
resolve()
|
||||
}
|
||||
signal?.addEventListener('abort', onAbort)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a request should be retried based on method and status code.
|
||||
*/
|
||||
function shouldRetry(
|
||||
method: string,
|
||||
status: number,
|
||||
attemptCount: number,
|
||||
retryEnabled: boolean
|
||||
): boolean {
|
||||
// Don't retry if retries are disabled or we've exhausted attempts
|
||||
if (!retryEnabled || attemptCount >= DEFAULT_MAX_RETRIES) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only retry idempotent methods (GET, HEAD, OPTIONS)
|
||||
if (!RETRYABLE_METHODS.includes(method as (typeof RETRYABLE_METHODS)[number])) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only retry on specific status codes (520 - Cloudflare errors)
|
||||
if (!RETRYABLE_STATUS_CODES.includes(status as (typeof RETRYABLE_STATUS_CODES)[number])) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default abstract class PostgrestBuilder<
|
||||
ClientOptions extends ClientServerOptions,
|
||||
Result,
|
||||
ThrowOnError extends boolean = false,
|
||||
> implements PromiseLike<
|
||||
ThrowOnError extends true ? PostgrestResponseSuccess<Result> : PostgrestSingleResponse<Result>
|
||||
> {
|
||||
protected method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE'
|
||||
protected url: URL
|
||||
protected headers: Headers
|
||||
protected schema?: string
|
||||
protected body?: unknown
|
||||
protected shouldThrowOnError = false
|
||||
protected signal?: AbortSignal
|
||||
protected fetch: Fetch
|
||||
protected isMaybeSingle: boolean
|
||||
protected shouldStripNulls: boolean
|
||||
protected urlLengthLimit: number
|
||||
|
||||
// Retry configuration - enabled by default
|
||||
protected retryEnabled: boolean = true
|
||||
|
||||
/**
|
||||
* Creates a builder configured for a specific PostgREST request.
|
||||
*
|
||||
* @example Using supabase-js (recommended)
|
||||
* ```ts
|
||||
* import { createClient } from '@supabase/supabase-js'
|
||||
*
|
||||
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
|
||||
* const { data, error } = await supabase.from('users').select('*')
|
||||
* ```
|
||||
*
|
||||
* @category Database
|
||||
*
|
||||
* @example Standalone import for bundle-sensitive environments
|
||||
* ```ts
|
||||
* import { PostgrestQueryBuilder } from '@supabase/postgrest-js'
|
||||
*
|
||||
* const builder = new PostgrestQueryBuilder(
|
||||
* new URL('https://xyzcompany.supabase.co/rest/v1/users'),
|
||||
* { headers: new Headers({ apikey: 'your-publishable-key' }) }
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
constructor(builder: {
|
||||
method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE'
|
||||
url: URL
|
||||
headers: HeadersInit
|
||||
schema?: string
|
||||
body?: unknown
|
||||
shouldThrowOnError?: boolean
|
||||
signal?: AbortSignal
|
||||
fetch?: Fetch
|
||||
isMaybeSingle?: boolean
|
||||
shouldStripNulls?: boolean
|
||||
urlLengthLimit?: number
|
||||
// Retry option
|
||||
retry?: boolean
|
||||
}) {
|
||||
this.method = builder.method
|
||||
this.url = builder.url
|
||||
this.headers = new Headers(builder.headers)
|
||||
this.schema = builder.schema
|
||||
this.body = builder.body
|
||||
this.shouldThrowOnError = builder.shouldThrowOnError ?? false
|
||||
this.signal = builder.signal
|
||||
this.isMaybeSingle = builder.isMaybeSingle ?? false
|
||||
this.shouldStripNulls = builder.shouldStripNulls ?? false
|
||||
this.urlLengthLimit = builder.urlLengthLimit ?? 8000
|
||||
this.retryEnabled = builder.retry ?? true
|
||||
|
||||
if (builder.fetch) {
|
||||
this.fetch = builder.fetch
|
||||
} else {
|
||||
this.fetch = fetch
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there's an error with the query, throwOnError will reject the promise by
|
||||
* throwing the error instead of returning it as part of a successful response.
|
||||
*
|
||||
* {@link https://github.com/supabase/supabase-js/issues/92}
|
||||
*
|
||||
* @category Database
|
||||
* @subcategory Using modifiers
|
||||
*/
|
||||
throwOnError(): PostgrestBuilder<ClientOptions, Result, true> {
|
||||
this.shouldThrowOnError = true
|
||||
return this as PostgrestBuilder<ClientOptions, Result, true>
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip null values from the response data. Properties with `null` values
|
||||
* will be omitted from the returned JSON objects.
|
||||
*
|
||||
* Requires PostgREST 11.2.0+.
|
||||
*
|
||||
* {@link https://docs.postgrest.org/en/stable/references/api/resource_representation.html#stripped-nulls}
|
||||
*
|
||||
* @category Database
|
||||
* @subcategory Using modifiers
|
||||
*
|
||||
* @example With `select()`
|
||||
* ```ts
|
||||
* const { data, error } = await supabase
|
||||
* .from('characters')
|
||||
* .select()
|
||||
* .stripNulls()
|
||||
* ```
|
||||
*
|
||||
* @exampleSql With `select()`
|
||||
* ```sql
|
||||
* create table
|
||||
* characters (id int8 primary key, name text, bio text);
|
||||
*
|
||||
* insert into
|
||||
* characters (id, name, bio)
|
||||
* values
|
||||
* (1, 'Luke', null),
|
||||
* (2, 'Leia', 'Princess of Alderaan');
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse With `select()`
|
||||
* ```json
|
||||
* {
|
||||
* "data": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "name": "Luke"
|
||||
* },
|
||||
* {
|
||||
* "id": 2,
|
||||
* "name": "Leia",
|
||||
* "bio": "Princess of Alderaan"
|
||||
* }
|
||||
* ],
|
||||
* "status": 200,
|
||||
* "statusText": "OK"
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
stripNulls(): this {
|
||||
if (this.headers.get('Accept') === 'text/csv') {
|
||||
throw new Error('stripNulls() cannot be used with csv()')
|
||||
}
|
||||
this.shouldStripNulls = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an HTTP header on this single PostgREST request, overriding any header
|
||||
* with the same name set on the client.
|
||||
*
|
||||
* This is an advanced escape hatch for one-off needs (passing a custom
|
||||
* `Authorization` for a single query, attaching a tracing header, etc.).
|
||||
* Most callers do not need it: configure client-wide headers via the
|
||||
* `headers` option when constructing the client, and authentication via
|
||||
* Supabase Auth.
|
||||
*
|
||||
* @param name - HTTP header name
|
||||
* @param value - HTTP header value
|
||||
*
|
||||
* @category Database
|
||||
* @subcategory Using modifiers
|
||||
*/
|
||||
setHeader(name: string, value: string): this {
|
||||
this.headers = new Headers(this.headers)
|
||||
this.headers.set(name, value)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @category Database
|
||||
* @subcategory Using modifiers
|
||||
*
|
||||
* Configure retry behavior for this request.
|
||||
*
|
||||
* By default, retries are enabled for idempotent requests (GET, HEAD, OPTIONS)
|
||||
* that fail with network errors or specific HTTP status codes (503, 520).
|
||||
* Retries use exponential backoff (1s, 2s, 4s) with a maximum of 3 attempts.
|
||||
*
|
||||
* @param enabled - Whether to enable retries for this request
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Disable retries for a specific query
|
||||
* const { data, error } = await supabase
|
||||
* .from('users')
|
||||
* .select()
|
||||
* .retry(false)
|
||||
* ```
|
||||
*/
|
||||
retry(enabled: boolean): this {
|
||||
this.retryEnabled = enabled
|
||||
return this
|
||||
}
|
||||
|
||||
then<
|
||||
TResult1 = ThrowOnError extends true
|
||||
? PostgrestResponseSuccess<Result>
|
||||
: PostgrestSingleResponse<Result>,
|
||||
TResult2 = never,
|
||||
>(
|
||||
onfulfilled?:
|
||||
| ((
|
||||
value: ThrowOnError extends true
|
||||
? PostgrestResponseSuccess<Result>
|
||||
: PostgrestSingleResponse<Result>
|
||||
) => TResult1 | PromiseLike<TResult1>)
|
||||
| undefined
|
||||
| null,
|
||||
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
|
||||
): PromiseLike<TResult1 | TResult2> {
|
||||
// https://postgrest.org/en/stable/api.html#switching-schemas
|
||||
if (this.schema === undefined) {
|
||||
// skip
|
||||
} else if (['GET', 'HEAD'].includes(this.method)) {
|
||||
this.headers.set('Accept-Profile', this.schema)
|
||||
} else {
|
||||
this.headers.set('Content-Profile', this.schema)
|
||||
}
|
||||
if (this.method !== 'GET' && this.method !== 'HEAD') {
|
||||
this.headers.set('Content-Type', 'application/json')
|
||||
}
|
||||
|
||||
// https://docs.postgrest.org/en/stable/references/api/resource_representation.html#stripped-nulls
|
||||
if (this.shouldStripNulls) {
|
||||
const currentAccept = this.headers.get('Accept')
|
||||
if (currentAccept === 'application/vnd.pgrst.object+json') {
|
||||
this.headers.set('Accept', 'application/vnd.pgrst.object+json;nulls=stripped')
|
||||
} else if (!currentAccept || currentAccept === 'application/json') {
|
||||
this.headers.set('Accept', 'application/vnd.pgrst.array+json;nulls=stripped')
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Invoke w/o `this` to avoid illegal invocation error.
|
||||
// https://github.com/supabase/postgrest-js/pull/247
|
||||
const _fetch = this.fetch
|
||||
|
||||
// Execute fetch with retry logic
|
||||
const executeWithRetry = async (): Promise<{
|
||||
error: any
|
||||
data: any
|
||||
count: number | null
|
||||
status: number
|
||||
statusText: string
|
||||
}> => {
|
||||
let attemptCount = 0
|
||||
|
||||
while (true) {
|
||||
// Serialize headers as a plain object rather than a Headers instance.
|
||||
// React Native's XHR-based fetch silently drops headers (notably Content-Type)
|
||||
// when given a Headers instance, causing PGRST202 on parameter-less RPC calls.
|
||||
// See supabase/supabase-js#1562 and facebook/react-native#33933.
|
||||
// All sibling packages (auth-js, storage-js, functions-js) already pass plain objects.
|
||||
const headers: Record<string, string> = {}
|
||||
this.headers.forEach((value, key) => {
|
||||
headers[key] = value
|
||||
})
|
||||
if (attemptCount > 0) {
|
||||
headers['X-Retry-Count'] = String(attemptCount)
|
||||
}
|
||||
|
||||
// Only wrap the fetch call itself — processResponse errors must never trigger retries
|
||||
let res: Response
|
||||
try {
|
||||
res = await _fetch(this.url.toString(), {
|
||||
method: this.method,
|
||||
headers,
|
||||
body: JSON.stringify(this.body, (_, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value
|
||||
),
|
||||
signal: this.signal,
|
||||
})
|
||||
// JS allows throwing any value, and serverless or realm-crossing fetch
|
||||
// implementations can reject with non-Error objects. `instanceof Error`
|
||||
// is too narrow here; narrow at the use site with optional chaining.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (fetchError: any) {
|
||||
// Never retry aborted requests
|
||||
if (fetchError?.name === 'AbortError' || fetchError?.code === 'ABORT_ERR') {
|
||||
throw fetchError
|
||||
}
|
||||
|
||||
// Don't retry network errors for non-idempotent methods
|
||||
if (!RETRYABLE_METHODS.includes(this.method as (typeof RETRYABLE_METHODS)[number])) {
|
||||
throw fetchError
|
||||
}
|
||||
|
||||
// Check if we should retry network errors
|
||||
if (this.retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
|
||||
const delay = getRetryDelay(attemptCount)
|
||||
attemptCount++
|
||||
await sleep(delay, this.signal)
|
||||
continue
|
||||
}
|
||||
|
||||
// Exhausted retries or retries disabled, throw the last error
|
||||
throw fetchError
|
||||
}
|
||||
|
||||
// Check if we should retry this HTTP response
|
||||
if (shouldRetry(this.method, res.status, attemptCount, this.retryEnabled)) {
|
||||
const retryAfterHeader = res.headers?.get('Retry-After') ?? null
|
||||
const delay =
|
||||
retryAfterHeader !== null
|
||||
? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1000
|
||||
: getRetryDelay(attemptCount)
|
||||
await res.text()
|
||||
attemptCount++
|
||||
await sleep(delay, this.signal)
|
||||
continue
|
||||
}
|
||||
|
||||
return await this.processResponse(res)
|
||||
}
|
||||
}
|
||||
|
||||
let res = executeWithRetry()
|
||||
|
||||
if (!this.shouldThrowOnError) {
|
||||
res = res.catch((fetchError) => {
|
||||
// Build detailed error information including cause if available
|
||||
// Note: We don't populate code/hint for client-side network errors since those
|
||||
// fields are meant for upstream service errors (PostgREST/PostgreSQL)
|
||||
let errorDetails = ''
|
||||
let hint = ''
|
||||
let code = ''
|
||||
|
||||
// Add cause information if available (e.g., DNS errors, network failures)
|
||||
const cause = fetchError?.cause
|
||||
if (cause) {
|
||||
const causeMessage = cause?.message ?? ''
|
||||
const causeCode = cause?.code ?? ''
|
||||
|
||||
errorDetails = `${fetchError?.name ?? 'FetchError'}: ${fetchError?.message}`
|
||||
errorDetails += `\n\nCaused by: ${cause?.name ?? 'Error'}: ${causeMessage}`
|
||||
if (causeCode) {
|
||||
errorDetails += ` (${causeCode})`
|
||||
}
|
||||
if (cause?.stack) {
|
||||
errorDetails += `\n${cause.stack}`
|
||||
}
|
||||
} else {
|
||||
// No cause available, just include the error stack
|
||||
errorDetails = fetchError?.stack ?? ''
|
||||
}
|
||||
|
||||
// Get URL length for potential hints
|
||||
const urlLength = this.url.toString().length
|
||||
|
||||
// Handle AbortError specially with helpful hints
|
||||
if (fetchError?.name === 'AbortError' || fetchError?.code === 'ABORT_ERR') {
|
||||
code = ''
|
||||
hint = 'Request was aborted (timeout or manual cancellation)'
|
||||
|
||||
if (urlLength > this.urlLengthLimit) {
|
||||
hint += `. Note: Your request URL is ${urlLength} characters, which may exceed server limits. If selecting many fields, consider using views. If filtering with large arrays (e.g., .in('id', [many IDs])), consider using an RPC function to pass values server-side.`
|
||||
}
|
||||
}
|
||||
// Handle HeadersOverflowError from undici (Node.js fetch implementation)
|
||||
else if (
|
||||
cause?.name === 'HeadersOverflowError' ||
|
||||
cause?.code === 'UND_ERR_HEADERS_OVERFLOW'
|
||||
) {
|
||||
code = ''
|
||||
hint = 'HTTP headers exceeded server limits (typically 16KB)'
|
||||
|
||||
if (urlLength > this.urlLengthLimit) {
|
||||
hint += `. Your request URL is ${urlLength} characters. If selecting many fields, consider using views. If filtering with large arrays (e.g., .in('id', [200+ IDs])), consider using an RPC function instead.`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false as const,
|
||||
error: {
|
||||
message: `${fetchError?.name ?? 'FetchError'}: ${fetchError?.message}`,
|
||||
details: errorDetails,
|
||||
hint: hint,
|
||||
code: code,
|
||||
},
|
||||
data: null,
|
||||
count: null,
|
||||
status: 0,
|
||||
statusText: '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
res as Promise<
|
||||
ThrowOnError extends true
|
||||
? PostgrestResponseSuccess<Result>
|
||||
: PostgrestSingleResponse<Result>
|
||||
>
|
||||
).then(onfulfilled, onrejected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a fetch response and return the standardized postgrest response.
|
||||
*/
|
||||
private async processResponse(res: Response): Promise<{
|
||||
success: boolean
|
||||
error: any
|
||||
data: any
|
||||
count: number | null
|
||||
status: number
|
||||
statusText: string
|
||||
}> {
|
||||
let error = null
|
||||
let data = null
|
||||
let count: number | null = null
|
||||
let status = res.status
|
||||
let statusText = res.statusText
|
||||
|
||||
if (res.ok) {
|
||||
if (this.method !== 'HEAD') {
|
||||
const body = await res.text()
|
||||
if (body === '') {
|
||||
// Prefer: return=minimal
|
||||
} else if (this.headers.get('Accept') === 'text/csv') {
|
||||
data = body
|
||||
} else if (
|
||||
this.headers.get('Accept') &&
|
||||
this.headers.get('Accept')?.includes('application/vnd.pgrst.plan+text')
|
||||
) {
|
||||
data = body
|
||||
} else {
|
||||
try {
|
||||
data = JSON.parse(body)
|
||||
} catch {
|
||||
// A 2xx status doesn't guarantee a JSON body; mirror the non-2xx fallback below.
|
||||
error = { message: body }
|
||||
data = null
|
||||
|
||||
if (this.shouldThrowOnError) {
|
||||
throw new PostgrestError({ message: body, details: '', hint: '', code: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const countHeader = this.headers.get('Prefer')?.match(/count=(exact|planned|estimated)/)
|
||||
const contentRange = res.headers.get('content-range')?.split('/')
|
||||
if (countHeader && contentRange && contentRange.length > 1) {
|
||||
count = parseInt(contentRange[1])
|
||||
}
|
||||
|
||||
// Fix for https://github.com/supabase/postgrest-js/issues/361 — applies to all methods.
|
||||
if (this.isMaybeSingle && Array.isArray(data)) {
|
||||
if (data.length > 1) {
|
||||
error = {
|
||||
// https://github.com/PostgREST/postgrest/blob/a867d79c42419af16c18c3fb019eba8df992626f/src/PostgREST/Error.hs#L553
|
||||
code: 'PGRST116',
|
||||
details: `Results contain ${data.length} rows, application/vnd.pgrst.object+json requires 1 row`,
|
||||
hint: null,
|
||||
message: 'JSON object requested, multiple (or no) rows returned',
|
||||
}
|
||||
data = null
|
||||
count = null
|
||||
status = 406
|
||||
statusText = 'Not Acceptable'
|
||||
} else if (data.length === 1) {
|
||||
data = data[0]
|
||||
} else {
|
||||
data = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const body = await res.text()
|
||||
|
||||
try {
|
||||
error = JSON.parse(body)
|
||||
|
||||
// Workaround for https://github.com/supabase/postgrest-js/issues/295
|
||||
if (Array.isArray(error) && res.status === 404) {
|
||||
data = []
|
||||
error = null
|
||||
status = 200
|
||||
statusText = 'OK'
|
||||
}
|
||||
} catch {
|
||||
// Workaround for https://github.com/supabase/postgrest-js/issues/295
|
||||
if (res.status === 404 && body === '') {
|
||||
status = 204
|
||||
statusText = 'No Content'
|
||||
} else {
|
||||
error = {
|
||||
message: body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error && this.shouldThrowOnError) {
|
||||
throw new PostgrestError(error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: error === null,
|
||||
error,
|
||||
data,
|
||||
count,
|
||||
status,
|
||||
statusText,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the type of the returned `data`.
|
||||
*
|
||||
* @typeParam NewResult - The new result type to override with
|
||||
* @deprecated Use overrideTypes<yourType, { merge: false }>() method at the end of your call chain instead
|
||||
*
|
||||
* @category Database
|
||||
* @subcategory Using modifiers
|
||||
*/
|
||||
returns<NewResult>(): PostgrestBuilder<
|
||||
ClientOptions,
|
||||
CheckMatchingArrayTypes<Result, NewResult>,
|
||||
ThrowOnError
|
||||
> {
|
||||
/* istanbul ignore next */
|
||||
return this as unknown as PostgrestBuilder<
|
||||
ClientOptions,
|
||||
CheckMatchingArrayTypes<Result, NewResult>,
|
||||
ThrowOnError
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the type of the returned `data` field in the response.
|
||||
*
|
||||
* @typeParam NewResult - The new type to cast the response data to
|
||||
* @typeParam Options - Optional type configuration (defaults to { merge: true })
|
||||
* @typeParam Options.merge - When true, merges the new type with existing return type. When false, replaces the existing types entirely (defaults to true)
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Merge with existing types (default behavior)
|
||||
* const query = supabase
|
||||
* .from('users')
|
||||
* .select()
|
||||
* .overrideTypes<{ custom_field: string }>()
|
||||
*
|
||||
* // Replace existing types completely
|
||||
* const replaceQuery = supabase
|
||||
* .from('users')
|
||||
* .select()
|
||||
* .overrideTypes<{ id: number; name: string }, { merge: false }>()
|
||||
* ```
|
||||
* @returns A PostgrestBuilder instance with the new type
|
||||
*
|
||||
* @category Database
|
||||
* @subcategory Using modifiers
|
||||
*
|
||||
* @example Complete Override type of successful response
|
||||
* ```ts
|
||||
* const { data } = await supabase
|
||||
* .from('countries')
|
||||
* .select()
|
||||
* .overrideTypes<Array<MyType>, { merge: false }>()
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Complete Override type of successful response
|
||||
* ```ts
|
||||
* let x: typeof data // MyType[]
|
||||
* ```
|
||||
*
|
||||
* @example Complete Override type of object response
|
||||
* ```ts
|
||||
* const { data } = await supabase
|
||||
* .from('countries')
|
||||
* .select()
|
||||
* .maybeSingle()
|
||||
* .overrideTypes<MyType, { merge: false }>()
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Complete Override type of object response
|
||||
* ```ts
|
||||
* let x: typeof data // MyType | null
|
||||
* ```
|
||||
*
|
||||
* @example Partial Override type of successful response
|
||||
* ```ts
|
||||
* const { data } = await supabase
|
||||
* .from('countries')
|
||||
* .select()
|
||||
* .overrideTypes<Array<{ status: "A" | "B" }>>()
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Partial Override type of successful response
|
||||
* ```ts
|
||||
* let x: typeof data // Array<CountryRowProperties & { status: "A" | "B" }>
|
||||
* ```
|
||||
*
|
||||
* @example Partial Override type of object response
|
||||
* ```ts
|
||||
* const { data } = await supabase
|
||||
* .from('countries')
|
||||
* .select()
|
||||
* .maybeSingle()
|
||||
* .overrideTypes<{ status: "A" | "B" }>()
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Partial Override type of object response
|
||||
* ```ts
|
||||
* let x: typeof data // CountryRowProperties & { status: "A" | "B" } | null
|
||||
* ```
|
||||
*
|
||||
* @example Merge vs replace existing types
|
||||
* ```typescript
|
||||
* // Merge with existing types (default behavior)
|
||||
* const query = supabase
|
||||
* .from('users')
|
||||
* .select()
|
||||
* .overrideTypes<{ custom_field: string }>()
|
||||
*
|
||||
* // Replace existing types completely
|
||||
* const replaceQuery = supabase
|
||||
* .from('users')
|
||||
* .select()
|
||||
* .overrideTypes<{ id: number; name: string }, { merge: false }>()
|
||||
* ```
|
||||
*/
|
||||
overrideTypes<
|
||||
NewResult,
|
||||
Options extends { merge?: boolean } = { merge: true },
|
||||
>(): PostgrestBuilder<
|
||||
ClientOptions,
|
||||
IsValidResultOverride<Result, NewResult, false, false> extends true
|
||||
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
|
||||
ContainsNull<Result> extends true
|
||||
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
|
||||
: MergePartialResult<NewResult, Result, Options>
|
||||
: CheckMatchingArrayTypes<Result, NewResult>,
|
||||
ThrowOnError
|
||||
> {
|
||||
return this as unknown as PostgrestBuilder<
|
||||
ClientOptions,
|
||||
IsValidResultOverride<Result, NewResult, false, false> extends true
|
||||
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
|
||||
ContainsNull<Result> extends true
|
||||
? MergePartialResult<NewResult, NonNullable<Result>, Options> | null
|
||||
: MergePartialResult<NewResult, Result, Options>
|
||||
: CheckMatchingArrayTypes<Result, NewResult>,
|
||||
ThrowOnError
|
||||
>
|
||||
}
|
||||
}
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
import PostgrestQueryBuilder from './PostgrestQueryBuilder'
|
||||
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
|
||||
import { Fetch, GenericSchema, ClientServerOptions } from './types/common/common'
|
||||
import { GetRpcFunctionFilterBuilderByArgs } from './types/common/rpc'
|
||||
|
||||
/**
|
||||
* PostgREST client.
|
||||
*
|
||||
* @typeParam Database - Types for the schema from the [type
|
||||
* generator](https://supabase.com/docs/reference/javascript/next/typescript-support)
|
||||
*
|
||||
* @typeParam SchemaName - Postgres schema to switch to. Must be a string
|
||||
* literal, the same one passed to the constructor. If the schema is not
|
||||
* `"public"`, this must be supplied manually.
|
||||
*/
|
||||
export default class PostgrestClient<
|
||||
Database = any,
|
||||
ClientOptions extends ClientServerOptions = Database extends {
|
||||
__InternalSupabase: infer I extends ClientServerOptions
|
||||
}
|
||||
? I
|
||||
: {},
|
||||
SchemaName extends string & keyof Omit<Database, '__InternalSupabase'> =
|
||||
'public' extends keyof Omit<Database, '__InternalSupabase'>
|
||||
? 'public'
|
||||
: string & keyof Omit<Database, '__InternalSupabase'>,
|
||||
Schema extends GenericSchema = Omit<
|
||||
Database,
|
||||
'__InternalSupabase'
|
||||
>[SchemaName] extends GenericSchema
|
||||
? Omit<Database, '__InternalSupabase'>[SchemaName]
|
||||
: any,
|
||||
> {
|
||||
url: string
|
||||
headers: Headers
|
||||
schemaName?: SchemaName
|
||||
fetch?: Fetch
|
||||
urlLengthLimit: number
|
||||
|
||||
// Retry configuration - enabled by default
|
||||
retry?: boolean
|
||||
|
||||
// TODO: Add back shouldThrowOnError once we figure out the typings
|
||||
/**
|
||||
* Creates a PostgREST client.
|
||||
*
|
||||
* @param url - URL of the PostgREST endpoint
|
||||
* @param options - Named parameters
|
||||
* @param options.headers - Custom headers
|
||||
* @param options.schema - Postgres schema to switch to
|
||||
* @param options.fetch - Custom fetch
|
||||
* @param options.timeout - Optional timeout in milliseconds for all requests. When set, requests will automatically abort after this duration to prevent indefinite hangs.
|
||||
* @param options.urlLengthLimit - Maximum URL length in characters before warnings/errors are triggered. Defaults to 8000.
|
||||
* @param options.retry - Enable or disable automatic retries for transient errors.
|
||||
* When enabled, idempotent requests (GET, HEAD, OPTIONS) that fail with network
|
||||
* errors or HTTP 503/520 responses will be automatically retried up to 3 times
|
||||
* with exponential backoff (1s, 2s, 4s). Defaults to `true`.
|
||||
* @example Using supabase-js (recommended)
|
||||
* ```ts
|
||||
* import { createClient } from '@supabase/supabase-js'
|
||||
*
|
||||
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
|
||||
* const { data, error } = await supabase.from('profiles').select('*')
|
||||
* ```
|
||||
*
|
||||
* @category Database
|
||||
*
|
||||
* @remarks
|
||||
* - A `timeout` option (in milliseconds) can be set to automatically abort requests that take too long.
|
||||
* - A `urlLengthLimit` option (default: 8000) can be set to control when URL length warnings are included in error messages for aborted requests.
|
||||
*
|
||||
* @example Standalone import for bundle-sensitive environments
|
||||
* ```ts
|
||||
* import { PostgrestClient } from '@supabase/postgrest-js'
|
||||
*
|
||||
* const postgrest = new PostgrestClient('https://xyzcompany.supabase.co/rest/v1', {
|
||||
* headers: { apikey: 'your-publishable-key' },
|
||||
* schema: 'public',
|
||||
* timeout: 30000, // 30 second timeout
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
constructor(
|
||||
url: string,
|
||||
{
|
||||
headers = {},
|
||||
schema,
|
||||
fetch,
|
||||
timeout,
|
||||
urlLengthLimit = 8000,
|
||||
retry,
|
||||
}: {
|
||||
headers?: HeadersInit
|
||||
schema?: SchemaName
|
||||
fetch?: Fetch
|
||||
timeout?: number
|
||||
urlLengthLimit?: number
|
||||
retry?: boolean
|
||||
} = {}
|
||||
) {
|
||||
this.url = url
|
||||
this.headers = new Headers(headers)
|
||||
this.schemaName = schema
|
||||
this.urlLengthLimit = urlLengthLimit
|
||||
|
||||
const originalFetch = fetch ?? globalThis.fetch
|
||||
|
||||
// Wrap fetch with timeout if specified
|
||||
if (timeout !== undefined && timeout > 0) {
|
||||
this.fetch = (input, init) => {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
// Merge abort signals if one already exists
|
||||
const existingSignal = init?.signal
|
||||
if (existingSignal) {
|
||||
// If the existing signal is already aborted, use it directly
|
||||
if (existingSignal.aborted) {
|
||||
clearTimeout(timeoutId)
|
||||
return originalFetch(input, init)
|
||||
}
|
||||
|
||||
// Listen to existing signal and abort our controller too
|
||||
const abortHandler = () => {
|
||||
clearTimeout(timeoutId)
|
||||
controller.abort()
|
||||
}
|
||||
existingSignal.addEventListener('abort', abortHandler, { once: true })
|
||||
|
||||
return originalFetch(input, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
}).finally(() => {
|
||||
clearTimeout(timeoutId)
|
||||
existingSignal.removeEventListener('abort', abortHandler)
|
||||
})
|
||||
}
|
||||
|
||||
return originalFetch(input, {
|
||||
...init,
|
||||
signal: controller.signal,
|
||||
}).finally(() => clearTimeout(timeoutId))
|
||||
}
|
||||
} else {
|
||||
this.fetch = originalFetch
|
||||
}
|
||||
this.retry = retry
|
||||
}
|
||||
/**
|
||||
* Perform a query on a table or a view.
|
||||
*
|
||||
* @param relation - The table or view name to query
|
||||
*
|
||||
* @category Database
|
||||
*/
|
||||
from<
|
||||
TableName extends string & keyof Schema['Tables'],
|
||||
Table extends Schema['Tables'][TableName],
|
||||
>(relation: TableName): PostgrestQueryBuilder<ClientOptions, Schema, Table, TableName>
|
||||
from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>(
|
||||
relation: ViewName
|
||||
): PostgrestQueryBuilder<ClientOptions, Schema, View, ViewName>
|
||||
from(
|
||||
relation: (string & keyof Schema['Tables']) | (string & keyof Schema['Views'])
|
||||
): PostgrestQueryBuilder<ClientOptions, Schema, any, any> {
|
||||
if (!relation || typeof relation !== 'string' || relation.trim() === '') {
|
||||
throw new Error('Invalid relation name: relation must be a non-empty string.')
|
||||
}
|
||||
|
||||
const url = new URL(`${this.url}/${relation}`)
|
||||
return new PostgrestQueryBuilder(url, {
|
||||
headers: new Headers(this.headers),
|
||||
schema: this.schemaName,
|
||||
fetch: this.fetch,
|
||||
urlLengthLimit: this.urlLengthLimit,
|
||||
retry: this.retry,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a schema to query or perform an function (rpc) call.
|
||||
*
|
||||
* The schema needs to be on the list of exposed schemas inside Supabase.
|
||||
*
|
||||
* @param schema - The schema to query
|
||||
*
|
||||
* @category Database
|
||||
*/
|
||||
schema<DynamicSchema extends string & keyof Omit<Database, '__InternalSupabase'>>(
|
||||
schema: DynamicSchema
|
||||
): PostgrestClient<
|
||||
Database,
|
||||
ClientOptions,
|
||||
DynamicSchema,
|
||||
Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
|
||||
> {
|
||||
return new PostgrestClient(this.url, {
|
||||
headers: this.headers,
|
||||
schema,
|
||||
fetch: this.fetch,
|
||||
urlLengthLimit: this.urlLengthLimit,
|
||||
retry: this.retry,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a function call.
|
||||
*
|
||||
* @param fn - The function name to call
|
||||
* @param args - The arguments to pass to the function call
|
||||
* @param options - Named parameters
|
||||
* @param options.head - When set to `true`, `data` will not be returned.
|
||||
* Useful if you only need the count.
|
||||
* @param options.get - When set to `true`, the function will be called with
|
||||
* read-only access mode.
|
||||
* @param options.count - Count algorithm to use to count rows returned by the
|
||||
* function. Only applicable for [set-returning
|
||||
* functions](https://www.postgresql.org/docs/current/functions-srf.html).
|
||||
*
|
||||
* `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
|
||||
* hood.
|
||||
*
|
||||
* `"planned"`: Approximated but fast count algorithm. Uses the Postgres
|
||||
* statistics under the hood.
|
||||
*
|
||||
* `"estimated"`: Uses exact count for low numbers and planned count for high
|
||||
* numbers.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // For cross-schema functions where type inference fails, use overrideTypes:
|
||||
* const { data } = await supabase
|
||||
* .schema('schema_b')
|
||||
* .rpc('function_a', {})
|
||||
* .overrideTypes<{ id: string; user_id: string }[]>()
|
||||
* ```
|
||||
*
|
||||
* @category Database
|
||||
*
|
||||
* @example Call a Postgres function without arguments
|
||||
* ```ts
|
||||
* const { data, error } = await supabase.rpc('hello_world')
|
||||
* ```
|
||||
*
|
||||
* @exampleSql Call a Postgres function without arguments
|
||||
* ```sql
|
||||
* create function hello_world() returns text as $$
|
||||
* select 'Hello world';
|
||||
* $$ language sql;
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Call a Postgres function without arguments
|
||||
* ```json
|
||||
* {
|
||||
* "data": "Hello world",
|
||||
* "status": 200,
|
||||
* "statusText": "OK"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Call a Postgres function with arguments
|
||||
* ```ts
|
||||
* const { data, error } = await supabase.rpc('echo', { say: '👋' })
|
||||
* ```
|
||||
*
|
||||
* @exampleSql Call a Postgres function with arguments
|
||||
* ```sql
|
||||
* create function echo(say text) returns text as $$
|
||||
* select say;
|
||||
* $$ language sql;
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Call a Postgres function with arguments
|
||||
* ```json
|
||||
* {
|
||||
* "data": "👋",
|
||||
* "status": 200,
|
||||
* "statusText": "OK"
|
||||
* }
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* @exampleDescription Bulk processing
|
||||
* You can process large payloads by passing in an array as an argument.
|
||||
*
|
||||
* @example Bulk processing
|
||||
* ```ts
|
||||
* const { data, error } = await supabase.rpc('add_one_each', { arr: [1, 2, 3] })
|
||||
* ```
|
||||
*
|
||||
* @exampleSql Bulk processing
|
||||
* ```sql
|
||||
* create function add_one_each(arr int[]) returns int[] as $$
|
||||
* select array_agg(n + 1) from unnest(arr) as n;
|
||||
* $$ language sql;
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Bulk processing
|
||||
* ```json
|
||||
* {
|
||||
* "data": [
|
||||
* 2,
|
||||
* 3,
|
||||
* 4
|
||||
* ],
|
||||
* "status": 200,
|
||||
* "statusText": "OK"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @exampleDescription Call a Postgres function with filters
|
||||
* Postgres functions that return tables can also be combined with [Filters](/docs/reference/javascript/using-filters) and [Modifiers](/docs/reference/javascript/using-modifiers).
|
||||
*
|
||||
* @example Call a Postgres function with filters
|
||||
* ```ts
|
||||
* const { data, error } = await supabase
|
||||
* .rpc('list_stored_countries')
|
||||
* .eq('id', 1)
|
||||
* .single()
|
||||
* ```
|
||||
*
|
||||
* @exampleSql Call a Postgres function with filters
|
||||
* ```sql
|
||||
* create table
|
||||
* countries (id int8 primary key, name text);
|
||||
*
|
||||
* insert into
|
||||
* countries (id, name)
|
||||
* values
|
||||
* (1, 'Rohan'),
|
||||
* (2, 'The Shire');
|
||||
*
|
||||
* create function list_stored_countries() returns setof countries as $$
|
||||
* select * from countries;
|
||||
* $$ language sql;
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Call a Postgres function with filters
|
||||
* ```json
|
||||
* {
|
||||
* "data": {
|
||||
* "id": 1,
|
||||
* "name": "Rohan"
|
||||
* },
|
||||
* "status": 200,
|
||||
* "statusText": "OK"
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example Call a read-only Postgres function
|
||||
* ```ts
|
||||
* const { data, error } = await supabase.rpc('hello_world', undefined, { get: true })
|
||||
* ```
|
||||
*
|
||||
* @exampleSql Call a read-only Postgres function
|
||||
* ```sql
|
||||
* create function hello_world() returns text as $$
|
||||
* select 'Hello world';
|
||||
* $$ language sql;
|
||||
* ```
|
||||
*
|
||||
* @exampleResponse Call a read-only Postgres function
|
||||
* ```json
|
||||
* {
|
||||
* "data": "Hello world",
|
||||
* "status": 200,
|
||||
* "statusText": "OK"
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
rpc<
|
||||
FnName extends string & keyof Schema['Functions'],
|
||||
Args extends Schema['Functions'][FnName]['Args'] = never,
|
||||
FilterBuilder extends GetRpcFunctionFilterBuilderByArgs<Schema, FnName, Args> =
|
||||
GetRpcFunctionFilterBuilderByArgs<Schema, FnName, Args>,
|
||||
>(
|
||||
fn: FnName,
|
||||
args: Args = {} as Args,
|
||||
{
|
||||
head = false,
|
||||
get = false,
|
||||
count,
|
||||
}: {
|
||||
head?: boolean
|
||||
get?: boolean
|
||||
count?: 'exact' | 'planned' | 'estimated' | (string & {})
|
||||
} = {}
|
||||
): PostgrestFilterBuilder<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
FilterBuilder['Row'],
|
||||
FilterBuilder['Result'],
|
||||
FilterBuilder['RelationName'],
|
||||
FilterBuilder['Relationships'],
|
||||
'RPC'
|
||||
> {
|
||||
let method: 'HEAD' | 'GET' | 'POST'
|
||||
const url = new URL(`${this.url}/rpc/${fn}`)
|
||||
let body: unknown | undefined
|
||||
// objects/arrays-of-objects can't be serialized to URL params, use POST + return=minimal instead
|
||||
const _isObject = (v: unknown): boolean =>
|
||||
v !== null && typeof v === 'object' && (!Array.isArray(v) || v.some(_isObject))
|
||||
const _hasObjectArg = head && Object.values(args as object).some(_isObject)
|
||||
if (_hasObjectArg) {
|
||||
method = 'POST'
|
||||
body = args
|
||||
} else if (head || get) {
|
||||
method = head ? 'HEAD' : 'GET'
|
||||
Object.entries(args)
|
||||
// params with undefined value needs to be filtered out, otherwise it'll
|
||||
// show up as `?param=undefined`
|
||||
.filter(([_, value]) => value !== undefined)
|
||||
// array values need special syntax
|
||||
.map(([name, value]) => [name, Array.isArray(value) ? `{${value.join(',')}}` : `${value}`])
|
||||
.forEach(([name, value]) => {
|
||||
url.searchParams.append(name, value)
|
||||
})
|
||||
} else {
|
||||
method = 'POST'
|
||||
body = args
|
||||
}
|
||||
|
||||
const headers = new Headers(this.headers)
|
||||
if (_hasObjectArg) {
|
||||
headers.set('Prefer', count ? `count=${count},return=minimal` : 'return=minimal')
|
||||
} else if (count) {
|
||||
headers.set('Prefer', `count=${count}`)
|
||||
}
|
||||
|
||||
return new PostgrestFilterBuilder({
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
schema: this.schemaName,
|
||||
body,
|
||||
fetch: this.fetch ?? fetch,
|
||||
urlLengthLimit: this.urlLengthLimit,
|
||||
retry: this.retry,
|
||||
})
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Error format
|
||||
*
|
||||
* Returned by every PostgREST request that fails. When something fails, the
|
||||
* single most useful field is usually `hint` — Postgres often returns the
|
||||
* actionable fix there, not in `message`. Always log the full object (e.g.
|
||||
* `console.error(error)`); logging only `error.message` hides the hint.
|
||||
*
|
||||
* Read the fields in roughly this order of usefulness:
|
||||
*
|
||||
* - `hint` — actionable guidance from the database when available. For
|
||||
* permission-denied errors (`42501`), this is the literal SQL to fix the
|
||||
* problem, e.g.
|
||||
* `"Grant the required privileges to the current role with: GRANT SELECT ON public.users TO anon;"`.
|
||||
* Missing column? `hint` suggests the column you probably meant. Whenever
|
||||
* Postgres knows the fix, it puts it in `hint`.
|
||||
* - `code` — stable error code from PostgREST (e.g. `PGRST301`) or Postgres
|
||||
* (e.g. `42501`). Branch on this rather than on `message` text.
|
||||
* - `details` — extra context, often the offending value, key, or row.
|
||||
* - `message` — human-readable summary. Useful in UI strings; less useful
|
||||
* for debugging.
|
||||
*
|
||||
* {@link https://postgrest.org/en/stable/api.html?highlight=options#errors-and-http-status-codes}
|
||||
*/
|
||||
export default class PostgrestError extends Error {
|
||||
details: string
|
||||
hint: string
|
||||
code: string
|
||||
|
||||
/**
|
||||
* @example
|
||||
* ```ts
|
||||
* import PostgrestError from '@supabase/postgrest-js'
|
||||
*
|
||||
* throw new PostgrestError({
|
||||
* message: 'Row level security prevented the request',
|
||||
* details: 'RLS denied the insert',
|
||||
* hint: 'Check your policies',
|
||||
* code: 'PGRST301',
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
constructor(context: { message: string; details: string; hint: string; code: string }) {
|
||||
super(context.message)
|
||||
this.name = 'PostgrestError'
|
||||
this.details = context.details
|
||||
this.hint = context.hint
|
||||
this.code = context.code
|
||||
}
|
||||
|
||||
toJSON(): { name: string; message: string; details: string; hint: string; code: string } {
|
||||
return {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
details: this.details,
|
||||
hint: this.hint,
|
||||
code: this.code,
|
||||
}
|
||||
}
|
||||
}
|
||||
+2190
File diff suppressed because it is too large
Load Diff
+1766
File diff suppressed because it is too large
Load Diff
+1056
File diff suppressed because it is too large
Load Diff
+2
@@ -0,0 +1,2 @@
|
||||
import { version } from './version'
|
||||
export const DEFAULT_HEADERS = { 'X-Client-Info': `postgrest-js/${version}` }
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import PostgrestClient from './PostgrestClient'
|
||||
import PostgrestQueryBuilder from './PostgrestQueryBuilder'
|
||||
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
|
||||
import PostgrestTransformBuilder from './PostgrestTransformBuilder'
|
||||
import PostgrestBuilder from './PostgrestBuilder'
|
||||
import PostgrestError from './PostgrestError'
|
||||
|
||||
export {
|
||||
PostgrestClient,
|
||||
PostgrestQueryBuilder,
|
||||
PostgrestFilterBuilder,
|
||||
PostgrestTransformBuilder,
|
||||
PostgrestBuilder,
|
||||
PostgrestError,
|
||||
}
|
||||
export default {
|
||||
PostgrestClient,
|
||||
PostgrestQueryBuilder,
|
||||
PostgrestFilterBuilder,
|
||||
PostgrestTransformBuilder,
|
||||
PostgrestBuilder,
|
||||
PostgrestError,
|
||||
}
|
||||
export type {
|
||||
PostgrestResponse,
|
||||
PostgrestResponseFailure,
|
||||
PostgrestResponseSuccess,
|
||||
PostgrestSingleResponse,
|
||||
PostgrestMaybeSingleResponse,
|
||||
} from './types/types'
|
||||
export type { ClientServerOptions as PostgrestClientOptions } from './types/common/common'
|
||||
// https://github.com/supabase/postgrest-js/issues/551
|
||||
// To be replaced with a helper type that only uses public types
|
||||
export type { GetResult as UnstableGetResult } from './select-query-parser/result'
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
// Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query)
|
||||
// See https://github.com/PostgREST/postgrest/blob/2f91853cb1de18944a4556df09e52450b881cfb3/src/PostgREST/ApiRequest/QueryParams.hs#L282-L284
|
||||
|
||||
import { SimplifyDeep } from '../types/types'
|
||||
import { JsonPathToAccessor } from './utils'
|
||||
|
||||
/**
|
||||
* Parses a query.
|
||||
* A query is a sequence of nodes, separated by `,`, ensuring that there is
|
||||
* no remaining input after all nodes have been parsed.
|
||||
*
|
||||
* Returns an array of parsed nodes, or an error.
|
||||
*/
|
||||
export type ParseQuery<Query extends string> = string extends Query
|
||||
? GenericStringError
|
||||
: ParseNodes<EatWhitespace<Query>> extends [infer Nodes, `${infer Remainder}`]
|
||||
? Nodes extends Ast.Node[]
|
||||
? EatWhitespace<Remainder> extends ''
|
||||
? SimplifyDeep<Nodes>
|
||||
: ParserError<`Unexpected input: ${Remainder}`>
|
||||
: ParserError<'Invalid nodes array structure'>
|
||||
: ParseNodes<EatWhitespace<Query>>
|
||||
|
||||
/**
|
||||
* Notes: all `Parse*` types assume that their input strings have their whitespace
|
||||
* removed. They return tuples of ["Return Value", "Remainder of text"] or
|
||||
* a `ParserError`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a sequence of nodes, separated by `,`.
|
||||
*
|
||||
* Returns a tuple of ["Parsed fields", "Remainder of text"] or an error.
|
||||
*/
|
||||
type ParseNodes<Input extends string> = string extends Input
|
||||
? GenericStringError
|
||||
: ParseNodesHelper<Input, []>
|
||||
|
||||
type ParseNodesHelper<Input extends string, Nodes extends Ast.Node[]> =
|
||||
ParseNode<Input> extends [infer Node, `${infer Remainder}`]
|
||||
? Node extends Ast.Node
|
||||
? EatWhitespace<Remainder> extends `,${infer Remainder}`
|
||||
? ParseNodesHelper<EatWhitespace<Remainder>, [...Nodes, Node]>
|
||||
: [[...Nodes, Node], EatWhitespace<Remainder>]
|
||||
: ParserError<'Invalid node type in nodes helper'>
|
||||
: ParseNode<Input>
|
||||
/**
|
||||
* Parses a node.
|
||||
* A node is one of the following:
|
||||
* - `*`
|
||||
* - a field, as defined above
|
||||
* - a renamed field, `renamed_field:field`
|
||||
* - a spread field, `...field`
|
||||
*/
|
||||
type ParseNode<Input extends string> = Input extends ''
|
||||
? ParserError<'Empty string'>
|
||||
: // `*`
|
||||
Input extends `*${infer Remainder}`
|
||||
? [Ast.StarNode, EatWhitespace<Remainder>]
|
||||
: // `...field`
|
||||
Input extends `...${infer Remainder}`
|
||||
? ParseField<EatWhitespace<Remainder>> extends [infer TargetField, `${infer Remainder}`]
|
||||
? TargetField extends Ast.FieldNode
|
||||
? [{ type: 'spread'; target: TargetField }, EatWhitespace<Remainder>]
|
||||
: ParserError<'Invalid target field type in spread'>
|
||||
: ParserError<`Unable to parse spread resource at \`${Input}\``>
|
||||
: ParseIdentifier<Input> extends [infer NameOrAlias, `${infer Remainder}`]
|
||||
? EatWhitespace<Remainder> extends `::${infer _}`
|
||||
? // It's a type cast and not an alias, so treat it as part of the field.
|
||||
ParseField<Input>
|
||||
: EatWhitespace<Remainder> extends `:${infer Remainder}`
|
||||
? // `alias:`
|
||||
ParseField<EatWhitespace<Remainder>> extends [infer Field, `${infer Remainder}`]
|
||||
? Field extends Ast.FieldNode
|
||||
? [Omit<Field, 'alias'> & { alias: NameOrAlias }, EatWhitespace<Remainder>]
|
||||
: ParserError<'Invalid field type in alias parsing'>
|
||||
: ParserError<`Unable to parse renamed field at \`${Input}\``>
|
||||
: // Otherwise, just parse it as a field without alias.
|
||||
ParseField<Input>
|
||||
: ParserError<`Expected identifier at \`${Input}\``>
|
||||
|
||||
/**
|
||||
* Parses a field without preceding alias.
|
||||
* A field is one of the following:
|
||||
* - a top-level `count` field: https://docs.postgrest.org/en/v12/references/api/aggregate_functions.html#the-case-of-count
|
||||
* - a field with an embedded resource
|
||||
* - `field(nodes)`
|
||||
* - `field!hint(nodes)`
|
||||
* - `field!inner(nodes)`
|
||||
* - `field!left(nodes)`
|
||||
* - `field!hint!inner(nodes)`
|
||||
* - `field!hint!left(nodes)`
|
||||
* - a field without an embedded resource (see {@link ParseNonEmbeddedResourceField})
|
||||
*/
|
||||
type ParseField<Input extends string> = Input extends ''
|
||||
? ParserError<'Empty string'>
|
||||
: ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
|
||||
? Name extends 'count'
|
||||
? ParseCountField<Input>
|
||||
: Remainder extends `!inner${infer Remainder}`
|
||||
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
|
||||
infer Children,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? Children extends Ast.Node[]
|
||||
? // `field!inner(nodes)`
|
||||
[{ type: 'field'; name: Name; innerJoin: true; children: Children }, Remainder]
|
||||
: ParserError<'Invalid children array in inner join'>
|
||||
: CreateParserErrorIfRequired<
|
||||
ParseEmbeddedResource<EatWhitespace<Remainder>>,
|
||||
`Expected embedded resource after "!inner" at \`${Remainder}\``
|
||||
>
|
||||
: EatWhitespace<Remainder> extends `!left${infer Remainder}`
|
||||
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
|
||||
infer Children,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? Children extends Ast.Node[]
|
||||
? // `field!left(nodes)`
|
||||
// !left is a noise word - treat it the same way as a non-`!inner`.
|
||||
[{ type: 'field'; name: Name; children: Children }, EatWhitespace<Remainder>]
|
||||
: ParserError<'Invalid children array in left join'>
|
||||
: CreateParserErrorIfRequired<
|
||||
ParseEmbeddedResource<EatWhitespace<Remainder>>,
|
||||
`Expected embedded resource after "!left" at \`${EatWhitespace<Remainder>}\``
|
||||
>
|
||||
: EatWhitespace<Remainder> extends `!${infer Remainder}`
|
||||
? ParseIdentifier<EatWhitespace<Remainder>> extends [infer Hint, `${infer Remainder}`]
|
||||
? EatWhitespace<Remainder> extends `!inner${infer Remainder}`
|
||||
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
|
||||
infer Children,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? Children extends Ast.Node[]
|
||||
? // `field!hint!inner(nodes)`
|
||||
[
|
||||
{
|
||||
type: 'field'
|
||||
name: Name
|
||||
hint: Hint
|
||||
innerJoin: true
|
||||
children: Children
|
||||
},
|
||||
EatWhitespace<Remainder>,
|
||||
]
|
||||
: ParserError<'Invalid children array in hint inner join'>
|
||||
: ParseEmbeddedResource<EatWhitespace<Remainder>>
|
||||
: ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
|
||||
infer Children,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? Children extends Ast.Node[]
|
||||
? // `field!hint(nodes)`
|
||||
[
|
||||
{ type: 'field'; name: Name; hint: Hint; children: Children },
|
||||
EatWhitespace<Remainder>,
|
||||
]
|
||||
: ParserError<'Invalid children array in hint'>
|
||||
: ParseEmbeddedResource<EatWhitespace<Remainder>>
|
||||
: ParserError<`Expected identifier after "!" at \`${EatWhitespace<Remainder>}\``>
|
||||
: EatWhitespace<Remainder> extends `(${infer _}`
|
||||
? ParseEmbeddedResource<EatWhitespace<Remainder>> extends [
|
||||
infer Children,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? Children extends Ast.Node[]
|
||||
? // `field(nodes)`
|
||||
[{ type: 'field'; name: Name; children: Children }, EatWhitespace<Remainder>]
|
||||
: ParserError<'Invalid children array in field'>
|
||||
: // Return error if start of embedded resource was detected but not found.
|
||||
ParseEmbeddedResource<EatWhitespace<Remainder>>
|
||||
: // Otherwise it's a non-embedded resource field.
|
||||
ParseNonEmbeddedResourceField<Input>
|
||||
: ParserError<`Expected identifier at \`${Input}\``>
|
||||
|
||||
type ParseCountField<Input extends string> =
|
||||
ParseIdentifier<Input> extends ['count', `${infer Remainder}`]
|
||||
? (
|
||||
EatWhitespace<Remainder> extends `()${infer Remainder_}`
|
||||
? EatWhitespace<Remainder_>
|
||||
: EatWhitespace<Remainder>
|
||||
) extends `${infer Remainder}`
|
||||
? Remainder extends `::${infer _}`
|
||||
? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
|
||||
? [
|
||||
{ type: 'field'; name: 'count'; aggregateFunction: 'count'; castType: CastType },
|
||||
Remainder,
|
||||
]
|
||||
: ParseFieldTypeCast<Remainder>
|
||||
: [{ type: 'field'; name: 'count'; aggregateFunction: 'count' }, Remainder]
|
||||
: never
|
||||
: ParserError<`Expected "count" at \`${Input}\``>
|
||||
|
||||
/**
|
||||
* Parses an embedded resource, which is an opening `(`, followed by a sequence of
|
||||
* 0 or more nodes separated by `,`, then a closing `)`.
|
||||
*
|
||||
* Returns a tuple of ["Parsed fields", "Remainder of text"], an error,
|
||||
* or the original string input indicating that no opening `(` was found.
|
||||
*/
|
||||
type ParseEmbeddedResource<Input extends string> = Input extends `(${infer Remainder}`
|
||||
? EatWhitespace<Remainder> extends `)${infer Remainder}`
|
||||
? [[], EatWhitespace<Remainder>]
|
||||
: ParseNodes<EatWhitespace<Remainder>> extends [infer Nodes, `${infer Remainder}`]
|
||||
? Nodes extends Ast.Node[]
|
||||
? EatWhitespace<Remainder> extends `)${infer Remainder}`
|
||||
? [Nodes, EatWhitespace<Remainder>]
|
||||
: ParserError<`Expected ")" at \`${EatWhitespace<Remainder>}\``>
|
||||
: ParserError<'Invalid nodes array in embedded resource'>
|
||||
: ParseNodes<EatWhitespace<Remainder>>
|
||||
: ParserError<`Expected "(" at \`${Input}\``>
|
||||
|
||||
/**
|
||||
* Parses a field excluding embedded resources, without preceding field renaming.
|
||||
* This is one of the following:
|
||||
* - `field`
|
||||
* - `field.aggregate()`
|
||||
* - `field.aggregate()::type`
|
||||
* - `field::type`
|
||||
* - `field::type.aggregate()`
|
||||
* - `field::type.aggregate()::type`
|
||||
* - `field->json...`
|
||||
* - `field->json.aggregate()`
|
||||
* - `field->json.aggregate()::type`
|
||||
* - `field->json::type`
|
||||
* - `field->json::type.aggregate()`
|
||||
* - `field->json::type.aggregate()::type`
|
||||
*/
|
||||
type ParseNonEmbeddedResourceField<Input extends string> =
|
||||
ParseIdentifier<Input> extends [infer Name, `${infer Remainder}`]
|
||||
? // Parse optional JSON path.
|
||||
(
|
||||
Remainder extends `->${infer PathAndRest}`
|
||||
? ParseJsonAccessor<Remainder> extends [
|
||||
infer PropertyName,
|
||||
infer PropertyType,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? [
|
||||
{
|
||||
type: 'field'
|
||||
name: Name
|
||||
alias: PropertyName
|
||||
castType: PropertyType
|
||||
jsonPath: JsonPathToAccessor<
|
||||
PathAndRest extends `${infer Path},${string}` ? Path : PathAndRest
|
||||
>
|
||||
},
|
||||
Remainder,
|
||||
]
|
||||
: ParseJsonAccessor<Remainder>
|
||||
: [{ type: 'field'; name: Name }, Remainder]
|
||||
) extends infer Parsed
|
||||
? Parsed extends [infer Field, `${infer Remainder}`]
|
||||
? // Parse optional typecast or aggregate function input typecast.
|
||||
(
|
||||
Remainder extends `::${infer _}`
|
||||
? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
|
||||
? [Omit<Field, 'castType'> & { castType: CastType }, Remainder]
|
||||
: ParseFieldTypeCast<Remainder>
|
||||
: [Field, Remainder]
|
||||
) extends infer Parsed
|
||||
? Parsed extends [infer Field, `${infer Remainder}`]
|
||||
? // Parse optional aggregate function.
|
||||
Remainder extends `.${infer _}`
|
||||
? ParseFieldAggregation<Remainder> extends [
|
||||
infer AggregateFunction,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? // Parse optional aggregate function output typecast.
|
||||
Remainder extends `::${infer _}`
|
||||
? ParseFieldTypeCast<Remainder> extends [infer CastType, `${infer Remainder}`]
|
||||
? [
|
||||
Omit<Field, 'castType'> & {
|
||||
aggregateFunction: AggregateFunction
|
||||
castType: CastType
|
||||
},
|
||||
Remainder,
|
||||
]
|
||||
: ParseFieldTypeCast<Remainder>
|
||||
: [Field & { aggregateFunction: AggregateFunction }, Remainder]
|
||||
: ParseFieldAggregation<Remainder>
|
||||
: [Field, Remainder]
|
||||
: Parsed
|
||||
: never
|
||||
: Parsed
|
||||
: never
|
||||
: ParserError<`Expected identifier at \`${Input}\``>
|
||||
|
||||
/**
|
||||
* Parses a JSON property accessor of the shape `->a->b->c`. The last accessor in
|
||||
* the series may convert to text by using the ->> operator instead of ->.
|
||||
*
|
||||
* Returns a tuple of ["Last property name", "Last property type", "Remainder of text"]
|
||||
*/
|
||||
type ParseJsonAccessor<Input extends string> = Input extends `->${infer Remainder}`
|
||||
? Remainder extends `>${infer Remainder}`
|
||||
? ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
|
||||
? [Name, 'text', EatWhitespace<Remainder>]
|
||||
: ParserError<'Expected property name after `->>`'>
|
||||
: ParseIdentifier<Remainder> extends [infer Name, `${infer Remainder}`]
|
||||
? ParseJsonAccessor<Remainder> extends [
|
||||
infer PropertyName,
|
||||
infer PropertyType,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? [PropertyName, PropertyType, EatWhitespace<Remainder>]
|
||||
: [Name, 'json', EatWhitespace<Remainder>]
|
||||
: ParserError<'Expected property name after `->`'>
|
||||
: ParserError<'Expected ->'>
|
||||
|
||||
/**
|
||||
* Parses a field typecast (`::type`), returning a tuple of ["Type", "Remainder of text"].
|
||||
*/
|
||||
type ParseFieldTypeCast<Input extends string> =
|
||||
EatWhitespace<Input> extends `::${infer Remainder}`
|
||||
? ParseIdentifier<EatWhitespace<Remainder>> extends [`${infer CastType}`, `${infer Remainder}`]
|
||||
? [CastType, EatWhitespace<Remainder>]
|
||||
: ParserError<`Invalid type for \`::\` operator at \`${Remainder}\``>
|
||||
: ParserError<'Expected ::'>
|
||||
|
||||
/**
|
||||
* Parses a field aggregation (`.max()`), returning a tuple of ["Aggregate function", "Remainder of text"]
|
||||
*/
|
||||
type ParseFieldAggregation<Input extends string> =
|
||||
EatWhitespace<Input> extends `.${infer Remainder}`
|
||||
? ParseIdentifier<EatWhitespace<Remainder>> extends [
|
||||
`${infer FunctionName}`,
|
||||
`${infer Remainder}`,
|
||||
]
|
||||
? // Ensure that aggregation function is valid.
|
||||
FunctionName extends Token.AggregateFunction
|
||||
? EatWhitespace<Remainder> extends `()${infer Remainder}`
|
||||
? [FunctionName, EatWhitespace<Remainder>]
|
||||
: ParserError<`Expected \`()\` after \`.\` operator \`${FunctionName}\``>
|
||||
: ParserError<`Invalid type for \`.\` operator \`${FunctionName}\``>
|
||||
: ParserError<`Invalid type for \`.\` operator at \`${Remainder}\``>
|
||||
: ParserError<'Expected .'>
|
||||
|
||||
/**
|
||||
* Parses a (possibly double-quoted) identifier.
|
||||
* Identifiers are sequences of 1 or more letters.
|
||||
*/
|
||||
type ParseIdentifier<Input extends string> =
|
||||
ParseLetters<Input> extends [infer Name, `${infer Remainder}`]
|
||||
? [Name, EatWhitespace<Remainder>]
|
||||
: ParseQuotedLetters<Input> extends [infer Name, `${infer Remainder}`]
|
||||
? [Name, EatWhitespace<Remainder>]
|
||||
: ParserError<`No (possibly double-quoted) identifier at \`${Input}\``>
|
||||
|
||||
/**
|
||||
* Parse a consecutive sequence of 1 or more letter, where letters are `[0-9a-zA-Z_]`.
|
||||
*/
|
||||
type ParseLetters<Input extends string> = string extends Input
|
||||
? GenericStringError
|
||||
: ParseLettersHelper<Input, ''> extends [`${infer Letters}`, `${infer Remainder}`]
|
||||
? Letters extends ''
|
||||
? ParserError<`Expected letter at \`${Input}\``>
|
||||
: [Letters, Remainder]
|
||||
: ParseLettersHelper<Input, ''>
|
||||
|
||||
type ParseLettersHelper<Input extends string, Acc extends string> = string extends Input
|
||||
? GenericStringError
|
||||
: Input extends `${infer L}${infer Remainder}`
|
||||
? L extends Token.Letter
|
||||
? ParseLettersHelper<Remainder, `${Acc}${L}`>
|
||||
: [Acc, Input]
|
||||
: [Acc, '']
|
||||
|
||||
/**
|
||||
* Parse a consecutive sequence of 1 or more double-quoted letters,
|
||||
* where letters are `[^"]`.
|
||||
*/
|
||||
type ParseQuotedLetters<Input extends string> = string extends Input
|
||||
? GenericStringError
|
||||
: Input extends `"${infer Remainder}`
|
||||
? ParseQuotedLettersHelper<Remainder, ''> extends [`${infer Letters}`, `${infer Remainder}`]
|
||||
? Letters extends ''
|
||||
? ParserError<`Expected string at \`${Remainder}\``>
|
||||
: [Letters, Remainder]
|
||||
: ParseQuotedLettersHelper<Remainder, ''>
|
||||
: ParserError<`Not a double-quoted string at \`${Input}\``>
|
||||
|
||||
type ParseQuotedLettersHelper<Input extends string, Acc extends string> = string extends Input
|
||||
? GenericStringError
|
||||
: Input extends `${infer L}${infer Remainder}`
|
||||
? L extends '"'
|
||||
? [Acc, Remainder]
|
||||
: ParseQuotedLettersHelper<Remainder, `${Acc}${L}`>
|
||||
: ParserError<`Missing closing double-quote in \`"${Acc}${Input}\``>
|
||||
|
||||
/**
|
||||
* Trims whitespace from the left of the input.
|
||||
*/
|
||||
type EatWhitespace<Input extends string> = string extends Input
|
||||
? GenericStringError
|
||||
: Input extends `${Token.Whitespace}${infer Remainder}`
|
||||
? EatWhitespace<Remainder>
|
||||
: Input
|
||||
|
||||
/**
|
||||
* Creates a new {@link ParserError} if the given input is not already a parser error.
|
||||
*/
|
||||
type CreateParserErrorIfRequired<Input, Message extends string> =
|
||||
Input extends ParserError<string> ? Input : ParserError<Message>
|
||||
|
||||
/**
|
||||
* Parser errors.
|
||||
*/
|
||||
export type ParserError<Message extends string> = { error: true } & Message
|
||||
type GenericStringError = ParserError<'Received a generic string'>
|
||||
|
||||
export namespace Ast {
|
||||
export type Node = FieldNode | StarNode | SpreadNode
|
||||
|
||||
export type FieldNode = {
|
||||
type: 'field'
|
||||
name: string
|
||||
alias?: string
|
||||
hint?: string
|
||||
innerJoin?: true
|
||||
castType?: string
|
||||
jsonPath?: string
|
||||
aggregateFunction?: Token.AggregateFunction
|
||||
children?: Node[]
|
||||
}
|
||||
|
||||
export type StarNode = {
|
||||
type: 'star'
|
||||
}
|
||||
|
||||
export type SpreadNode = {
|
||||
type: 'spread'
|
||||
target: FieldNode & { children: Node[] }
|
||||
}
|
||||
}
|
||||
|
||||
namespace Token {
|
||||
export type Whitespace = ' ' | '\n' | '\t'
|
||||
|
||||
type LowerAlphabet =
|
||||
| 'a'
|
||||
| 'b'
|
||||
| 'c'
|
||||
| 'd'
|
||||
| 'e'
|
||||
| 'f'
|
||||
| 'g'
|
||||
| 'h'
|
||||
| 'i'
|
||||
| 'j'
|
||||
| 'k'
|
||||
| 'l'
|
||||
| 'm'
|
||||
| 'n'
|
||||
| 'o'
|
||||
| 'p'
|
||||
| 'q'
|
||||
| 'r'
|
||||
| 's'
|
||||
| 't'
|
||||
| 'u'
|
||||
| 'v'
|
||||
| 'w'
|
||||
| 'x'
|
||||
| 'y'
|
||||
| 'z'
|
||||
|
||||
type Alphabet = LowerAlphabet | Uppercase<LowerAlphabet>
|
||||
|
||||
type Digit = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0'
|
||||
|
||||
export type Letter = Alphabet | Digit | '_'
|
||||
|
||||
export type AggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'
|
||||
}
|
||||
+556
@@ -0,0 +1,556 @@
|
||||
import { Ast, ParseQuery } from './parser'
|
||||
import {
|
||||
AggregateFunctions,
|
||||
ExtractFirstProperty,
|
||||
GenericSchema,
|
||||
IsNonEmptyArray,
|
||||
Prettify,
|
||||
TablesAndViews,
|
||||
TypeScriptTypes,
|
||||
ContainsNull,
|
||||
GenericRelationship,
|
||||
PostgreSQLTypes,
|
||||
GenericTable,
|
||||
ClientServerOptions,
|
||||
} from './types'
|
||||
import {
|
||||
CheckDuplicateEmbededReference,
|
||||
GetComputedFields,
|
||||
GetFieldNodeResultName,
|
||||
IsAny,
|
||||
IsRelationNullable,
|
||||
IsStringUnion,
|
||||
JsonPathToType,
|
||||
ResolveRelationship,
|
||||
SelectQueryError,
|
||||
} from './utils'
|
||||
import type { SpreadOnManyEnabled } from '../types/feature-flags'
|
||||
|
||||
/**
|
||||
* Main entry point for constructing the result type of a PostgREST query.
|
||||
*
|
||||
* @param Schema - Database schema.
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current table or view.
|
||||
* @param Relationships - Relationships of the current table.
|
||||
* @param Query - The select query string literal to parse.
|
||||
*/
|
||||
export type GetResult<
|
||||
Schema extends GenericSchema,
|
||||
Row extends Record<string, unknown>,
|
||||
RelationName,
|
||||
Relationships,
|
||||
Query extends string,
|
||||
ClientOptions extends ClientServerOptions,
|
||||
> =
|
||||
IsAny<Schema> extends true
|
||||
? ParseQuery<Query> extends infer ParsedQuery
|
||||
? ParsedQuery extends Ast.Node[]
|
||||
? RelationName extends string
|
||||
? ProcessNodesWithoutSchema<ParsedQuery>
|
||||
: any
|
||||
: ParsedQuery
|
||||
: any
|
||||
: Relationships extends null // For .rpc calls the passed relationships will be null in that case, the result will always be the function return type
|
||||
? ParseQuery<Query> extends infer ParsedQuery
|
||||
? ParsedQuery extends Ast.Node[]
|
||||
? RPCCallNodes<ParsedQuery, RelationName extends string ? RelationName : 'rpc_call', Row>
|
||||
: ParsedQuery
|
||||
: Row
|
||||
: ParseQuery<Query> extends infer ParsedQuery
|
||||
? ParsedQuery extends Ast.Node[]
|
||||
? RelationName extends string
|
||||
? Relationships extends GenericRelationship[]
|
||||
? ProcessNodes<ClientOptions, Schema, Row, RelationName, Relationships, ParsedQuery>
|
||||
: SelectQueryError<'Invalid Relationships cannot infer result type'>
|
||||
: SelectQueryError<'Invalid RelationName cannot infer result type'>
|
||||
: ParsedQuery
|
||||
: never
|
||||
|
||||
type ProcessSimpleFieldWithoutSchema<Field extends Ast.FieldNode> =
|
||||
Field['aggregateFunction'] extends AggregateFunctions
|
||||
? {
|
||||
// An aggregate function will always override the column name id.sum() will become sum
|
||||
// except if it has been aliased
|
||||
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes
|
||||
? TypeScriptTypes<Field['castType']>
|
||||
: number
|
||||
}
|
||||
: {
|
||||
// Aliases override the property name in the result
|
||||
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes // We apply the detected casted as the result type
|
||||
? TypeScriptTypes<Field['castType']>
|
||||
: any
|
||||
}
|
||||
|
||||
type ProcessFieldNodeWithoutSchema<Node extends Ast.FieldNode> =
|
||||
IsNonEmptyArray<Node['children']> extends true
|
||||
? {
|
||||
[K in GetFieldNodeResultName<Node>]: Node['children'] extends Ast.Node[]
|
||||
? ProcessNodesWithoutSchema<Node['children']>[]
|
||||
: ProcessSimpleFieldWithoutSchema<Node>
|
||||
}
|
||||
: ProcessSimpleFieldWithoutSchema<Node>
|
||||
|
||||
/**
|
||||
* Processes a single Node without schema and returns the resulting TypeScript type.
|
||||
*/
|
||||
type ProcessNodeWithoutSchema<Node extends Ast.Node> = Node extends Ast.StarNode
|
||||
? any
|
||||
: Node extends Ast.SpreadNode
|
||||
? Node['target']['children'] extends Ast.StarNode[]
|
||||
? any
|
||||
: Node['target']['children'] extends Ast.FieldNode[]
|
||||
? {
|
||||
[P in Node['target']['children'][number] as GetFieldNodeResultName<P>]: P['castType'] extends PostgreSQLTypes
|
||||
? TypeScriptTypes<P['castType']>
|
||||
: any
|
||||
}
|
||||
: any
|
||||
: Node extends Ast.FieldNode
|
||||
? ProcessFieldNodeWithoutSchema<Node>
|
||||
: any
|
||||
|
||||
/**
|
||||
* Processes nodes when Schema is any, providing basic type inference
|
||||
*/
|
||||
type ProcessNodesWithoutSchema<
|
||||
Nodes extends Ast.Node[],
|
||||
Acc extends Record<string, unknown> = {},
|
||||
> = Nodes extends [infer FirstNode, ...infer RestNodes]
|
||||
? FirstNode extends Ast.Node
|
||||
? RestNodes extends Ast.Node[]
|
||||
? ProcessNodeWithoutSchema<FirstNode> extends infer FieldResult
|
||||
? FieldResult extends Record<string, unknown>
|
||||
? ProcessNodesWithoutSchema<RestNodes, Acc & FieldResult>
|
||||
: FieldResult
|
||||
: any
|
||||
: any
|
||||
: any
|
||||
: Prettify<Acc>
|
||||
|
||||
/**
|
||||
* Processes a single Node from a select chained after a rpc call
|
||||
*
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current rpc function
|
||||
* @param NodeType - The Node to process.
|
||||
*/
|
||||
export type ProcessRPCNode<
|
||||
Row extends Record<string, unknown>,
|
||||
RelationName extends string,
|
||||
NodeType extends Ast.Node,
|
||||
> = NodeType['type'] extends Ast.StarNode['type'] // If the selection is *
|
||||
? Row
|
||||
: NodeType['type'] extends Ast.FieldNode['type']
|
||||
? ProcessSimpleField<Row, RelationName, Extract<NodeType, Ast.FieldNode>>
|
||||
: SelectQueryError<'RPC Unsupported node type.'>
|
||||
|
||||
/**
|
||||
* Process select call that can be chained after an rpc call
|
||||
*/
|
||||
export type RPCCallNodes<
|
||||
Nodes extends Ast.Node[],
|
||||
RelationName extends string,
|
||||
Row extends Record<string, unknown>,
|
||||
Acc extends Record<string, unknown> = {}, // Acc is now an object
|
||||
> = Nodes extends [infer FirstNode, ...infer RestNodes]
|
||||
? FirstNode extends Ast.Node
|
||||
? RestNodes extends Ast.Node[]
|
||||
? ProcessRPCNode<Row, RelationName, FirstNode> extends infer FieldResult
|
||||
? FieldResult extends Record<string, unknown>
|
||||
? RPCCallNodes<RestNodes, RelationName, Row, Acc & FieldResult>
|
||||
: FieldResult extends SelectQueryError<infer E>
|
||||
? SelectQueryError<E>
|
||||
: SelectQueryError<'Could not retrieve a valid record or error value'>
|
||||
: SelectQueryError<'Processing node failed.'>
|
||||
: SelectQueryError<'Invalid rest nodes array in RPC call'>
|
||||
: SelectQueryError<'Invalid first node in RPC call'>
|
||||
: Prettify<Acc>
|
||||
|
||||
/**
|
||||
* Recursively processes an array of Nodes and accumulates the resulting TypeScript type.
|
||||
*
|
||||
* @param Schema - Database schema.
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current table or view.
|
||||
* @param Relationships - Relationships of the current table.
|
||||
* @param Nodes - An array of AST nodes to process.
|
||||
* @param Acc - Accumulator for the constructed type.
|
||||
*/
|
||||
export type ProcessNodes<
|
||||
ClientOptions extends ClientServerOptions,
|
||||
Schema extends GenericSchema,
|
||||
Row extends Record<string, unknown>,
|
||||
RelationName extends string,
|
||||
Relationships extends GenericRelationship[],
|
||||
Nodes extends Ast.Node[],
|
||||
Acc extends Record<string, unknown> = {}, // Acc is now an object
|
||||
> =
|
||||
CheckDuplicateEmbededReference<Schema, RelationName, Relationships, Nodes> extends false
|
||||
? Nodes extends [infer FirstNode, ...infer RestNodes]
|
||||
? FirstNode extends Ast.Node
|
||||
? RestNodes extends Ast.Node[]
|
||||
? ProcessNode<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
Row,
|
||||
RelationName,
|
||||
Relationships,
|
||||
FirstNode
|
||||
> extends infer FieldResult
|
||||
? FieldResult extends Record<string, unknown>
|
||||
? ProcessNodes<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
Row,
|
||||
RelationName,
|
||||
Relationships,
|
||||
RestNodes,
|
||||
// TODO:
|
||||
// This SHOULD be `Omit<Acc, keyof FieldResult> & FieldResult` since in the case where the key
|
||||
// is present in the Acc already, the intersection will create bad intersection types
|
||||
// (eg: `{ a: number } & { a: { property } }` will become `{ a: number & { property } }`)
|
||||
// but using Omit here explode the inference complexity resulting in "infinite recursion error" from typescript
|
||||
// very early (see: 'Check that selecting many fields doesn't yield an possibly infinite recursion error') test
|
||||
// in this case we can't get above ~10 fields before reaching the recursion error
|
||||
// If someone find a better way to do this, please do it !
|
||||
// It'll also allow to fix those two tests:
|
||||
// - `'join over a 1-M relation with both nullables and non-nullables fields using column name hinting on nested relation'`
|
||||
// - `'self reference relation via column''`
|
||||
Acc & FieldResult
|
||||
>
|
||||
: FieldResult extends SelectQueryError<infer E>
|
||||
? SelectQueryError<E>
|
||||
: SelectQueryError<'Could not retrieve a valid record or error value'>
|
||||
: SelectQueryError<'Processing node failed.'>
|
||||
: SelectQueryError<'Invalid rest nodes array type in ProcessNodes'>
|
||||
: SelectQueryError<'Invalid first node type in ProcessNodes'>
|
||||
: Prettify<Acc>
|
||||
: Prettify<CheckDuplicateEmbededReference<Schema, RelationName, Relationships, Nodes>>
|
||||
|
||||
/**
|
||||
* Processes a single Node and returns the resulting TypeScript type.
|
||||
*
|
||||
* @param Schema - Database schema.
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current table or view.
|
||||
* @param Relationships - Relationships of the current table.
|
||||
* @param NodeType - The Node to process.
|
||||
*/
|
||||
export type ProcessNode<
|
||||
ClientOptions extends ClientServerOptions,
|
||||
Schema extends GenericSchema,
|
||||
Row extends Record<string, unknown>,
|
||||
RelationName extends string,
|
||||
Relationships extends GenericRelationship[],
|
||||
NodeType extends Ast.Node,
|
||||
> =
|
||||
// TODO: figure out why comparing the `type` property is necessary vs. `NodeType extends Ast.StarNode`
|
||||
NodeType['type'] extends Ast.StarNode['type'] // If the selection is *
|
||||
? // If the row has computed field, postgrest will omit them from star selection per default
|
||||
GetComputedFields<Schema, RelationName> extends never
|
||||
? // If no computed fields are detected on the row, we can return it as is
|
||||
Row
|
||||
: // otherwise we omit all the computed field from the star result return
|
||||
Omit<Row, GetComputedFields<Schema, RelationName>>
|
||||
: NodeType['type'] extends Ast.SpreadNode['type'] // If the selection is a ...spread
|
||||
? ProcessSpreadNode<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
Row,
|
||||
RelationName,
|
||||
Relationships,
|
||||
Extract<NodeType, Ast.SpreadNode>
|
||||
>
|
||||
: NodeType['type'] extends Ast.FieldNode['type']
|
||||
? ProcessFieldNode<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
Row,
|
||||
RelationName,
|
||||
Relationships,
|
||||
Extract<NodeType, Ast.FieldNode>
|
||||
>
|
||||
: SelectQueryError<'Unsupported node type.'>
|
||||
|
||||
/**
|
||||
* Processes a FieldNode and returns the resulting TypeScript type.
|
||||
*
|
||||
* @param Schema - Database schema.
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current table or view.
|
||||
* @param Relationships - Relationships of the current table.
|
||||
* @param Field - The FieldNode to process.
|
||||
*/
|
||||
type ProcessFieldNode<
|
||||
ClientOptions extends ClientServerOptions,
|
||||
Schema extends GenericSchema,
|
||||
Row extends Record<string, unknown>,
|
||||
RelationName extends string,
|
||||
Relationships extends GenericRelationship[],
|
||||
Field extends Ast.FieldNode,
|
||||
> = Field['children'] extends []
|
||||
? // Empty `()` — could be a scalar computed column (e.g. `user_count()`).
|
||||
// Route through ProcessEmbeddedResource so the scalar path returns the correct
|
||||
// primitive type. For non-scalar embedded resources with empty selection,
|
||||
// ProcessEmbeddedResource returns {} (no contribution to result type).
|
||||
ProcessEmbeddedResource<ClientOptions, Schema, Relationships, Field, RelationName>
|
||||
: IsNonEmptyArray<Field['children']> extends true // Has embedded resource?
|
||||
? ProcessEmbeddedResource<ClientOptions, Schema, Relationships, Field, RelationName>
|
||||
: ProcessSimpleField<Row, RelationName, Field>
|
||||
|
||||
type ResolveJsonPathType<
|
||||
Value,
|
||||
Path extends string | undefined,
|
||||
CastType extends PostgreSQLTypes,
|
||||
> = Path extends string
|
||||
? JsonPathToType<Value, Path> extends never
|
||||
? // Always fallback if JsonPathToType returns never
|
||||
TypeScriptTypes<CastType>
|
||||
: JsonPathToType<Value, Path> extends infer PathResult
|
||||
? PathResult extends string
|
||||
? // Use the result if it's a string as we know that even with the string accessor ->> it's a valid type
|
||||
PathResult
|
||||
: IsStringUnion<PathResult> extends true
|
||||
? // Use the result if it's a union of strings
|
||||
PathResult
|
||||
: CastType extends 'json'
|
||||
? // If the type is not a string, ensure it was accessed with json accessor ->
|
||||
PathResult
|
||||
: // Otherwise it means non-string value accessed with string accessor ->> use the TypeScriptTypes result
|
||||
TypeScriptTypes<CastType>
|
||||
: TypeScriptTypes<CastType>
|
||||
: // No json path, use regular type casting
|
||||
TypeScriptTypes<CastType>
|
||||
|
||||
/**
|
||||
* Processes a simple field (without embedded resources).
|
||||
*
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current table or view.
|
||||
* @param Field - The FieldNode to process.
|
||||
*/
|
||||
type ProcessSimpleField<
|
||||
Row extends Record<string, unknown>,
|
||||
RelationName extends string,
|
||||
Field extends Ast.FieldNode,
|
||||
> = Field['name'] extends keyof Row | 'count'
|
||||
? Field['aggregateFunction'] extends AggregateFunctions
|
||||
? {
|
||||
// An aggregate function will always override the column name id.sum() will become sum
|
||||
// except if it has been aliased
|
||||
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes
|
||||
? TypeScriptTypes<Field['castType']>
|
||||
: number
|
||||
}
|
||||
: {
|
||||
// Aliases override the property name in the result
|
||||
[K in GetFieldNodeResultName<Field>]: Field['castType'] extends PostgreSQLTypes
|
||||
? ResolveJsonPathType<Row[Field['name']], Field['jsonPath'], Field['castType']>
|
||||
: Row[Field['name']]
|
||||
}
|
||||
: SelectQueryError<`column '${Field['name']}' does not exist on '${RelationName}'.`>
|
||||
|
||||
/**
|
||||
* Processes an embedded resource (relation).
|
||||
*
|
||||
* @param Schema - Database schema.
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current table or view.
|
||||
* @param Relationships - Relationships of the current table.
|
||||
* @param Field - The FieldNode to process.
|
||||
*/
|
||||
export type ProcessEmbeddedResource<
|
||||
ClientOptions extends ClientServerOptions,
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
Field extends Ast.FieldNode,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
> =
|
||||
ResolveRelationship<Schema, Relationships, Field, CurrentTableOrView> extends infer Resolved
|
||||
? Resolved extends {
|
||||
scalarType: infer ScalarType
|
||||
relation: { isSetofReturn?: boolean; isNotNullable?: boolean }
|
||||
}
|
||||
? // Scalar computed column: bypass ProcessNodes and return the primitive type directly.
|
||||
{
|
||||
[K in GetFieldNodeResultName<Field>]: Resolved['relation']['isSetofReturn'] extends true
|
||||
? ScalarType
|
||||
: Resolved['relation']['isNotNullable'] extends true
|
||||
? ScalarType
|
||||
: ScalarType | null
|
||||
}
|
||||
: Resolved extends {
|
||||
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
|
||||
relation: GenericRelationship & { match: 'refrel' | 'col' | 'fkname' | 'func' }
|
||||
direction: string
|
||||
}
|
||||
? Field['children'] extends []
|
||||
? // Empty `()` on a regular table embed — no fields selected, contribute nothing.
|
||||
// This preserves the prior behavior: `users()` does not add a `users` key to the result.
|
||||
{}
|
||||
: ProcessEmbeddedResourceResult<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
Resolved,
|
||||
Field,
|
||||
CurrentTableOrView
|
||||
>
|
||||
: // Otherwise the Resolved is a SelectQueryError return it
|
||||
{ [K in GetFieldNodeResultName<Field>]: Resolved }
|
||||
: {
|
||||
[K in GetFieldNodeResultName<Field>]: SelectQueryError<'Failed to resolve relationship.'> &
|
||||
string
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper type to process the result of an embedded resource.
|
||||
*/
|
||||
type ProcessEmbeddedResourceResult<
|
||||
ClientOptions extends ClientServerOptions,
|
||||
Schema extends GenericSchema,
|
||||
Resolved extends {
|
||||
referencedTable: Pick<GenericTable, 'Row' | 'Relationships'>
|
||||
relation: GenericRelationship & {
|
||||
match: 'refrel' | 'col' | 'fkname' | 'func'
|
||||
isNotNullable?: boolean
|
||||
referencedRelation: string
|
||||
isSetofReturn?: boolean
|
||||
}
|
||||
direction: string
|
||||
},
|
||||
Field extends Ast.FieldNode,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema>,
|
||||
> =
|
||||
ProcessNodes<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
Resolved['referencedTable']['Row'],
|
||||
// For embeded function selection, the source of truth is the 'referencedRelation'
|
||||
// coming from the SetofOptions.to parameter
|
||||
Resolved['relation']['match'] extends 'func'
|
||||
? Resolved['relation']['referencedRelation']
|
||||
: Field['name'],
|
||||
Resolved['referencedTable']['Relationships'],
|
||||
Field['children'] extends undefined
|
||||
? []
|
||||
: Exclude<Field['children'], undefined> extends Ast.Node[]
|
||||
? Exclude<Field['children'], undefined>
|
||||
: []
|
||||
> extends infer ProcessedChildren
|
||||
? {
|
||||
[K in GetFieldNodeResultName<Field>]: Resolved['direction'] extends 'forward'
|
||||
? Field extends { innerJoin: true }
|
||||
? Resolved['relation']['isOneToOne'] extends true
|
||||
? ProcessedChildren
|
||||
: ProcessedChildren[]
|
||||
: Resolved['relation']['isOneToOne'] extends true
|
||||
? Resolved['relation']['match'] extends 'func'
|
||||
? Resolved['relation']['isNotNullable'] extends true
|
||||
? Resolved['relation']['isSetofReturn'] extends true
|
||||
? ProcessedChildren
|
||||
: // TODO: This shouldn't be necessary but is due in an inconsitency in PostgREST v12/13 where if a function
|
||||
// is declared with RETURNS <table-name> instead of RETURNS SETOF <table-name> ROWS 1
|
||||
// In case where there is no object matching the relations, the object will be returned with all the properties within it
|
||||
// set to null, we mimic this buggy behavior for type safety an issue is opened on postgREST here:
|
||||
// https://github.com/PostgREST/postgrest/issues/4234
|
||||
{ [P in keyof ProcessedChildren]: ProcessedChildren[P] | null }
|
||||
: ProcessedChildren | null
|
||||
: ProcessedChildren | null
|
||||
: ProcessedChildren[]
|
||||
: // If the relation is a self-reference it'll always be considered as a reverse relationship.
|
||||
// PostgREST returns arrays for a self-reference when using the table name
|
||||
// (collections(*)) or any hint (collections!parent_id(*)), and a single object
|
||||
// only when using the column name directly (parent_id(*)).
|
||||
// See https://github.com/PostgREST/postgrest/blob/8776ece7d5fd612fad44151f9be4dffd855b28f0/src/PostgREST/Plan.hs#L633-L644
|
||||
Resolved['relation']['referencedRelation'] extends CurrentTableOrView
|
||||
? // An explicit hint must be checked before `match: 'col'`, since a hinted
|
||||
// self-reference is always an array (eg: collections!parent_id(*)).
|
||||
Resolved['relation'] extends { hint: string }
|
||||
? ProcessedChildren[]
|
||||
: // Without a hint, a column inclusion (eg: parent_id(*)) is a single object.
|
||||
Resolved['relation']['match'] extends 'col'
|
||||
? IsRelationNullable<
|
||||
TablesAndViews<Schema>[CurrentTableOrView],
|
||||
Resolved['relation']
|
||||
> extends true
|
||||
? ProcessedChildren | null
|
||||
: ProcessedChildren
|
||||
: // The table name (eg: collections(*)) is an array of children.
|
||||
ProcessedChildren[]
|
||||
: // Otherwise if it's a non self-reference reverse relationship it's a single object
|
||||
IsRelationNullable<
|
||||
TablesAndViews<Schema>[CurrentTableOrView],
|
||||
Resolved['relation']
|
||||
> extends true
|
||||
? Field extends { innerJoin: true }
|
||||
? ProcessedChildren
|
||||
: ProcessedChildren | null
|
||||
: ProcessedChildren
|
||||
}
|
||||
: {
|
||||
[K in GetFieldNodeResultName<Field>]: SelectQueryError<'Failed to process embedded resource nodes.'> &
|
||||
string
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a SpreadNode by processing its target node.
|
||||
*
|
||||
* @param Schema - Database schema.
|
||||
* @param Row - The type of a row in the current table.
|
||||
* @param RelationName - The name of the current table or view.
|
||||
* @param Relationships - Relationships of the current table.
|
||||
* @param Spread - The SpreadNode to process.
|
||||
*/
|
||||
type ProcessSpreadNode<
|
||||
ClientOptions extends ClientServerOptions,
|
||||
Schema extends GenericSchema,
|
||||
Row extends Record<string, unknown>,
|
||||
RelationName extends string,
|
||||
Relationships extends GenericRelationship[],
|
||||
Spread extends Ast.SpreadNode,
|
||||
> =
|
||||
ProcessNode<
|
||||
ClientOptions,
|
||||
Schema,
|
||||
Row,
|
||||
RelationName,
|
||||
Relationships,
|
||||
Spread['target']
|
||||
> extends infer Result
|
||||
? Result extends SelectQueryError<infer E>
|
||||
? SelectQueryError<E>
|
||||
: ExtractFirstProperty<Result> extends unknown[]
|
||||
? SpreadOnManyEnabled<ClientOptions['PostgrestVersion']> extends true // Spread over an many-to-many relationship, turn all the result fields into correlated arrays
|
||||
? ProcessManyToManySpreadNodeResult<Result>
|
||||
: {
|
||||
[K in Spread['target']['name']]: SelectQueryError<`"${RelationName}" and "${Spread['target']['name']}" do not form a many-to-one or one-to-one relationship spread not possible`>
|
||||
}
|
||||
: ProcessSpreadNodeResult<Result>
|
||||
: never
|
||||
|
||||
/**
|
||||
* Helper type to process the result of a many-to-many spread node.
|
||||
* Converts all fields in the spread object into arrays.
|
||||
*/
|
||||
type ProcessManyToManySpreadNodeResult<Result> =
|
||||
Result extends Record<string, SelectQueryError<string> | null>
|
||||
? Result
|
||||
: ExtractFirstProperty<Result> extends infer SpreadedObject
|
||||
? SpreadedObject extends Array<Record<string, unknown>>
|
||||
? { [K in keyof SpreadedObject[number]]: Array<SpreadedObject[number][K]> }
|
||||
: SelectQueryError<'An error occurred spreading the many-to-many object'>
|
||||
: SelectQueryError<'An error occurred spreading the many-to-many object'>
|
||||
|
||||
/**
|
||||
* Helper type to process the result of a spread node.
|
||||
*/
|
||||
type ProcessSpreadNodeResult<Result> =
|
||||
Result extends Record<string, SelectQueryError<string> | null>
|
||||
? Result
|
||||
: ExtractFirstProperty<Result> extends infer SpreadedObject
|
||||
? ContainsNull<SpreadedObject> extends true
|
||||
? Exclude<{ [K in keyof SpreadedObject]: SpreadedObject[K] | null }, null>
|
||||
: Exclude<{ [K in keyof SpreadedObject]: SpreadedObject[K] }, null>
|
||||
: SelectQueryError<'An error occurred spreading the object'>
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import type {
|
||||
GenericRelationship,
|
||||
GenericSchema,
|
||||
GenericTable,
|
||||
ClientServerOptions,
|
||||
GenericSetofOption,
|
||||
GenericFunction,
|
||||
} from '../types/common/common'
|
||||
import type { Prettify } from '../types/types'
|
||||
|
||||
export type {
|
||||
GenericRelationship,
|
||||
GenericSchema,
|
||||
GenericTable,
|
||||
ClientServerOptions,
|
||||
GenericSetofOption,
|
||||
Prettify,
|
||||
GenericFunction,
|
||||
}
|
||||
|
||||
export type AggregateWithoutColumnFunctions = 'count'
|
||||
|
||||
export type AggregateWithColumnFunctions =
|
||||
| 'sum'
|
||||
| 'avg'
|
||||
| 'min'
|
||||
| 'max'
|
||||
| AggregateWithoutColumnFunctions
|
||||
|
||||
export type AggregateFunctions = AggregateWithColumnFunctions
|
||||
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| {
|
||||
[key: string]: Json | undefined
|
||||
}
|
||||
| Json[]
|
||||
|
||||
type PostgresSQLNumberTypes = 'int2' | 'int4' | 'int8' | 'float4' | 'float8' | 'numeric'
|
||||
|
||||
type PostgresSQLStringTypes =
|
||||
| 'bytea'
|
||||
| 'bpchar'
|
||||
| 'varchar'
|
||||
| 'date'
|
||||
| 'text'
|
||||
| 'citext'
|
||||
| 'time'
|
||||
| 'timetz'
|
||||
| 'timestamp'
|
||||
| 'timestamptz'
|
||||
| 'uuid'
|
||||
| 'vector'
|
||||
|
||||
type SingleValuePostgreSQLTypes =
|
||||
| PostgresSQLNumberTypes
|
||||
| PostgresSQLStringTypes
|
||||
| 'bool'
|
||||
| 'json'
|
||||
| 'jsonb'
|
||||
| 'void'
|
||||
| 'record'
|
||||
| string
|
||||
|
||||
type ArrayPostgreSQLTypes = `_${SingleValuePostgreSQLTypes}`
|
||||
|
||||
type TypeScriptSingleValueTypes<T extends SingleValuePostgreSQLTypes> = T extends 'bool'
|
||||
? boolean
|
||||
: T extends PostgresSQLNumberTypes
|
||||
? number
|
||||
: T extends PostgresSQLStringTypes
|
||||
? string
|
||||
: T extends 'json' | 'jsonb'
|
||||
? Json
|
||||
: T extends 'void'
|
||||
? undefined
|
||||
: T extends 'record'
|
||||
? Record<string, unknown>
|
||||
: unknown
|
||||
|
||||
type StripUnderscore<T extends string> = T extends `_${infer U}` ? U : T
|
||||
|
||||
// Represents all possible PostgreSQL types, including array types, allow for custom types with 'string' in union
|
||||
export type PostgreSQLTypes = SingleValuePostgreSQLTypes | ArrayPostgreSQLTypes
|
||||
|
||||
// Helper type to convert PostgreSQL types to their TypeScript equivalents
|
||||
export type TypeScriptTypes<T extends PostgreSQLTypes> = T extends ArrayPostgreSQLTypes
|
||||
? TypeScriptSingleValueTypes<StripUnderscore<Extract<T, SingleValuePostgreSQLTypes>>>[]
|
||||
: TypeScriptSingleValueTypes<T>
|
||||
|
||||
// Utility types for working with unions
|
||||
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I
|
||||
) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
export type LastOf<T> =
|
||||
UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never
|
||||
|
||||
export type Push<T extends any[], V> = [...T, V]
|
||||
|
||||
// Converts a union type to a tuple type
|
||||
export type UnionToTuple<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = N extends true
|
||||
? []
|
||||
: Push<UnionToTuple<Exclude<T, L>>, L>
|
||||
|
||||
export type UnionToArray<T> = UnionToTuple<T>
|
||||
|
||||
// Extracts the type of the first property in an object type
|
||||
export type ExtractFirstProperty<T> = T extends { [K in keyof T]: infer U } ? U : never
|
||||
|
||||
// Type predicates
|
||||
export type ContainsNull<T> = null extends T ? true : false
|
||||
|
||||
export type IsNonEmptyArray<T> =
|
||||
Exclude<T, undefined> extends readonly [unknown, ...unknown[]] ? true : false
|
||||
|
||||
// Types for working with database schemas
|
||||
export type TablesAndViews<Schema extends GenericSchema> = Schema['Tables'] &
|
||||
Exclude<Schema['Views'], ''>
|
||||
|
||||
export type GetTableRelationships<
|
||||
Schema extends GenericSchema,
|
||||
Tname extends string,
|
||||
> = TablesAndViews<Schema>[Tname] extends { Relationships: infer R } ? R : false
|
||||
+706
@@ -0,0 +1,706 @@
|
||||
import { Ast } from './parser'
|
||||
import {
|
||||
AggregateFunctions,
|
||||
ContainsNull,
|
||||
GenericRelationship,
|
||||
GenericSchema,
|
||||
GenericTable,
|
||||
IsNonEmptyArray,
|
||||
TablesAndViews,
|
||||
UnionToArray,
|
||||
GenericFunction,
|
||||
GenericSetofOption,
|
||||
} from './types'
|
||||
|
||||
export type IsAny<T> = 0 extends 1 & T ? true : false
|
||||
|
||||
export type SelectQueryError<Message extends string> = { error: true } & Message
|
||||
|
||||
/*
|
||||
** Because of pg-meta types generation there is some cases where a same relationship can be duplicated
|
||||
** if the relation is across schemas and views this ensure that we dedup those relations and treat them
|
||||
** as postgrest would.
|
||||
** This is no longer the case and has been patched here: https://github.com/supabase/postgres-meta/pull/809
|
||||
** But we still need this for retro-compatibilty with older generated types
|
||||
** TODO: Remove this in next major version
|
||||
*/
|
||||
export type DeduplicateRelationships<T extends readonly unknown[]> = T extends readonly [
|
||||
infer First,
|
||||
...infer Rest,
|
||||
]
|
||||
? First extends Rest[number]
|
||||
? DeduplicateRelationships<Rest extends readonly unknown[] ? Rest : []>
|
||||
: [First, ...DeduplicateRelationships<Rest extends readonly unknown[] ? Rest : []>]
|
||||
: T
|
||||
|
||||
export type GetFieldNodeResultName<Field extends Ast.FieldNode> = Field['alias'] extends string
|
||||
? Field['alias']
|
||||
: Field['aggregateFunction'] extends AggregateFunctions
|
||||
? Field['aggregateFunction']
|
||||
: Field['name']
|
||||
|
||||
type FilterRelationNodes<Nodes extends Ast.Node[]> = UnionToArray<
|
||||
{
|
||||
[K in keyof Nodes]: Nodes[K] extends Ast.SpreadNode
|
||||
? Nodes[K]['target']
|
||||
: Nodes[K] extends Ast.FieldNode
|
||||
? IsNonEmptyArray<Nodes[K]['children']> extends true
|
||||
? Nodes[K]
|
||||
: never
|
||||
: never
|
||||
}[number]
|
||||
>
|
||||
|
||||
type ResolveRelationships<
|
||||
Schema extends GenericSchema,
|
||||
RelationName extends string,
|
||||
Relationships extends GenericRelationship[],
|
||||
Nodes extends Ast.FieldNode[],
|
||||
> = UnionToArray<{
|
||||
[K in keyof Nodes]: Nodes[K] extends Ast.FieldNode
|
||||
? ResolveRelationship<Schema, Relationships, Nodes[K], RelationName> extends infer Relation
|
||||
? Relation extends {
|
||||
relation: {
|
||||
referencedRelation: string
|
||||
foreignKeyName: string
|
||||
match: string
|
||||
}
|
||||
from: string
|
||||
}
|
||||
? {
|
||||
referencedTable: Relation['relation']['referencedRelation']
|
||||
fkName: Relation['relation']['foreignKeyName']
|
||||
from: Relation['from']
|
||||
match: Relation['relation']['match']
|
||||
fieldName: GetFieldNodeResultName<Nodes[K]>
|
||||
}
|
||||
: Relation
|
||||
: never
|
||||
: never
|
||||
}>[0]
|
||||
|
||||
/**
|
||||
* Checks if a relation is implicitly referenced twice, requiring disambiguation
|
||||
*/
|
||||
type IsDoubleReference<T, U> = T extends {
|
||||
referencedTable: infer RT
|
||||
fieldName: infer FN
|
||||
match: infer M
|
||||
}
|
||||
? M extends 'col' | 'refrel'
|
||||
? U extends { referencedTable: RT; fieldName: FN; match: M }
|
||||
? true
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
|
||||
/**
|
||||
* Compares one element with all other elements in the array to find duplicates
|
||||
*/
|
||||
type CheckDuplicates<Arr extends any[], Current> = Arr extends [infer Head, ...infer Tail]
|
||||
? IsDoubleReference<Current, Head> extends true
|
||||
? Head | CheckDuplicates<Tail, Current> // Return the Head if duplicate
|
||||
: CheckDuplicates<Tail, Current> // Otherwise, continue checking
|
||||
: never
|
||||
|
||||
/**
|
||||
* Iterates over the elements of the array to find duplicates
|
||||
*/
|
||||
type FindDuplicatesWithinDeduplicated<Arr extends any[]> = Arr extends [infer Head, ...infer Tail]
|
||||
? CheckDuplicates<Tail, Head> | FindDuplicatesWithinDeduplicated<Tail>
|
||||
: never
|
||||
|
||||
type FindDuplicates<Arr extends any[]> = FindDuplicatesWithinDeduplicated<
|
||||
DeduplicateRelationships<Arr>
|
||||
>
|
||||
|
||||
export type CheckDuplicateEmbededReference<
|
||||
Schema extends GenericSchema,
|
||||
RelationName extends string,
|
||||
Relationships extends GenericRelationship[],
|
||||
Nodes extends Ast.Node[],
|
||||
> =
|
||||
FilterRelationNodes<Nodes> extends infer RelationsNodes
|
||||
? RelationsNodes extends Ast.FieldNode[]
|
||||
? ResolveRelationships<
|
||||
Schema,
|
||||
RelationName,
|
||||
Relationships,
|
||||
RelationsNodes
|
||||
> extends infer ResolvedRels
|
||||
? ResolvedRels extends unknown[]
|
||||
? FindDuplicates<ResolvedRels> extends infer Duplicates
|
||||
? Duplicates extends never
|
||||
? false
|
||||
: Duplicates extends { fieldName: infer FieldName }
|
||||
? FieldName extends string
|
||||
? {
|
||||
[K in FieldName]: SelectQueryError<`table "${RelationName}" specified more than once use hinting for desambiguation`>
|
||||
}
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
|
||||
/**
|
||||
* Returns a boolean representing whether there is a foreign key referencing
|
||||
* a given relation.
|
||||
*/
|
||||
type HasFKeyToFRel<FRelName, Relationships> = Relationships extends [infer R]
|
||||
? R extends { referencedRelation: FRelName }
|
||||
? true
|
||||
: false
|
||||
: Relationships extends [infer R, ...infer Rest]
|
||||
? HasFKeyToFRel<FRelName, [R]> extends true
|
||||
? true
|
||||
: HasFKeyToFRel<FRelName, Rest>
|
||||
: false
|
||||
/**
|
||||
* Checks if there is more than one relation to a given foreign relation name in the Relationships.
|
||||
*/
|
||||
type HasMultipleFKeysToFRelDeduplicated<FRelName, Relationships> = Relationships extends [
|
||||
infer R,
|
||||
...infer Rest,
|
||||
]
|
||||
? R extends { referencedRelation: FRelName }
|
||||
? HasFKeyToFRel<FRelName, Rest> extends true
|
||||
? true
|
||||
: HasMultipleFKeysToFRelDeduplicated<FRelName, Rest>
|
||||
: HasMultipleFKeysToFRelDeduplicated<FRelName, Rest>
|
||||
: false
|
||||
|
||||
type HasMultipleFKeysToFRel<
|
||||
FRelName,
|
||||
Relationships extends unknown[],
|
||||
> = HasMultipleFKeysToFRelDeduplicated<FRelName, DeduplicateRelationships<Relationships>>
|
||||
|
||||
type CheckRelationshipError<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
FoundRelation,
|
||||
> =
|
||||
FoundRelation extends SelectQueryError<string>
|
||||
? FoundRelation
|
||||
: // If the relation is a reverse relation with no hint (matching by name)
|
||||
FoundRelation extends {
|
||||
relation: {
|
||||
referencedRelation: infer RelatedRelationName
|
||||
name: string
|
||||
}
|
||||
direction: 'reverse'
|
||||
}
|
||||
? RelatedRelationName extends string
|
||||
? // We check if there is possible confusion with other relations with this table
|
||||
HasMultipleFKeysToFRel<RelatedRelationName, Relationships> extends true
|
||||
? FoundRelation extends { relation: { match: 'col' } }
|
||||
? FoundRelation
|
||||
: // If there is, postgrest will fail at runtime, and require desambiguation via hinting
|
||||
SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}!<columnName> ?`>
|
||||
: FoundRelation
|
||||
: never
|
||||
: // Same check for forward relationships, but we must gather the relationships from the found relation
|
||||
FoundRelation extends {
|
||||
relation: {
|
||||
referencedRelation: infer RelatedRelationName
|
||||
name: string
|
||||
}
|
||||
direction: 'forward'
|
||||
from: infer From
|
||||
}
|
||||
? RelatedRelationName extends string
|
||||
? From extends keyof TablesAndViews<Schema> & string
|
||||
? HasMultipleFKeysToFRel<
|
||||
RelatedRelationName,
|
||||
TablesAndViews<Schema>[From]['Relationships']
|
||||
> extends true
|
||||
? SelectQueryError<`Could not embed because more than one relationship was found for '${From}' and '${RelatedRelationName}' you need to hint the column with ${From}!<columnName> ?`>
|
||||
: FoundRelation
|
||||
: never
|
||||
: never
|
||||
: FoundRelation
|
||||
/**
|
||||
* Resolves relationships for embedded resources and retrieves the referenced Table
|
||||
*/
|
||||
export type ResolveRelationship<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
Field extends Ast.FieldNode,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
> =
|
||||
ResolveReverseRelationship<
|
||||
Schema,
|
||||
Relationships,
|
||||
Field,
|
||||
CurrentTableOrView
|
||||
> extends infer ReverseRelationship
|
||||
? ReverseRelationship extends false
|
||||
? CheckRelationshipError<
|
||||
Schema,
|
||||
Relationships,
|
||||
CurrentTableOrView,
|
||||
ResolveForwardRelationship<Schema, Field, CurrentTableOrView>
|
||||
>
|
||||
: CheckRelationshipError<Schema, Relationships, CurrentTableOrView, ReverseRelationship>
|
||||
: never
|
||||
|
||||
/**
|
||||
* Resolves reverse relationships (from children to parent)
|
||||
*/
|
||||
type ResolveReverseRelationship<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
Field extends Ast.FieldNode,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
> =
|
||||
FindFieldMatchingRelationships<Schema, Relationships, Field> extends infer FoundRelation
|
||||
? FoundRelation extends never
|
||||
? false
|
||||
: FoundRelation extends { referencedRelation: infer RelatedRelationName }
|
||||
? RelatedRelationName extends string
|
||||
? RelatedRelationName extends keyof TablesAndViews<Schema>
|
||||
? // If the relation was found via hinting or a column match, return it without further checks
|
||||
FoundRelation extends { hint: string } | { match: 'col' }
|
||||
? {
|
||||
referencedTable: TablesAndViews<Schema>[RelatedRelationName]
|
||||
relation: FoundRelation
|
||||
direction: 'reverse'
|
||||
from: CurrentTableOrView
|
||||
}
|
||||
: // If the relation was found via implicit relation naming, we must ensure there is no conflicting matches
|
||||
HasMultipleFKeysToFRel<RelatedRelationName, Relationships> extends true
|
||||
? SelectQueryError<`Could not embed because more than one relationship was found for '${RelatedRelationName}' and '${CurrentTableOrView}' you need to hint the column with ${RelatedRelationName}!<columnName> ?`>
|
||||
: {
|
||||
referencedTable: TablesAndViews<Schema>[RelatedRelationName]
|
||||
relation: FoundRelation
|
||||
direction: 'reverse'
|
||||
from: CurrentTableOrView
|
||||
}
|
||||
: SelectQueryError<`Relation '${RelatedRelationName}' not found in schema.`>
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
|
||||
export type FindMatchingTableRelationships<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
value extends string,
|
||||
> = Relationships extends [infer R, ...infer Rest]
|
||||
? Rest extends GenericRelationship[]
|
||||
? R extends { referencedRelation: infer ReferencedRelation }
|
||||
? ReferencedRelation extends keyof Schema['Tables']
|
||||
? R extends { foreignKeyName: value }
|
||||
? R & { match: 'fkname' }
|
||||
: R extends { referencedRelation: value }
|
||||
? R & { match: 'refrel' }
|
||||
: R extends { columns: [value] }
|
||||
? R & { match: 'col' }
|
||||
: FindMatchingTableRelationships<Schema, Rest, value>
|
||||
: FindMatchingTableRelationships<Schema, Rest, value>
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
|
||||
export type FindMatchingViewRelationships<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
value extends string,
|
||||
> = Relationships extends [infer R, ...infer Rest]
|
||||
? Rest extends GenericRelationship[]
|
||||
? R extends { referencedRelation: infer ReferencedRelation }
|
||||
? ReferencedRelation extends keyof Schema['Views']
|
||||
? R extends { foreignKeyName: value }
|
||||
? R & { match: 'fkname' }
|
||||
: R extends { referencedRelation: value }
|
||||
? R & { match: 'refrel' }
|
||||
: R extends { columns: [value] }
|
||||
? R & { match: 'col' }
|
||||
: FindMatchingViewRelationships<Schema, Rest, value>
|
||||
: FindMatchingViewRelationships<Schema, Rest, value>
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
|
||||
export type FindMatchingHintTableRelationships<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
hint extends string,
|
||||
name extends string,
|
||||
> = Relationships extends [infer R, ...infer Rest]
|
||||
? Rest extends GenericRelationship[]
|
||||
? R extends { referencedRelation: infer ReferencedRelation }
|
||||
? ReferencedRelation extends name
|
||||
? R extends { foreignKeyName: hint }
|
||||
? R & { match: 'fkname' }
|
||||
: R extends { referencedRelation: hint }
|
||||
? R & { match: 'refrel' }
|
||||
: R extends { columns: [hint] }
|
||||
? R & { match: 'col' }
|
||||
: FindMatchingHintTableRelationships<Schema, Rest, hint, name>
|
||||
: FindMatchingHintTableRelationships<Schema, Rest, hint, name>
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
export type FindMatchingHintViewRelationships<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
hint extends string,
|
||||
name extends string,
|
||||
> = Relationships extends [infer R, ...infer Rest]
|
||||
? Rest extends GenericRelationship[]
|
||||
? R extends { referencedRelation: infer ReferencedRelation }
|
||||
? ReferencedRelation extends name
|
||||
? R extends { foreignKeyName: hint }
|
||||
? R & { match: 'fkname' }
|
||||
: R extends { referencedRelation: hint }
|
||||
? R & { match: 'refrel' }
|
||||
: R extends { columns: [hint] }
|
||||
? R & { match: 'col' }
|
||||
: FindMatchingHintViewRelationships<Schema, Rest, hint, name>
|
||||
: FindMatchingHintViewRelationships<Schema, Rest, hint, name>
|
||||
: false
|
||||
: false
|
||||
: false
|
||||
|
||||
type IsColumnsNullable<
|
||||
Table extends Pick<GenericTable, 'Row'>,
|
||||
Columns extends (keyof Table['Row'])[],
|
||||
> = Columns extends [infer Column, ...infer Rest]
|
||||
? Column extends keyof Table['Row']
|
||||
? ContainsNull<Table['Row'][Column]> extends true
|
||||
? true
|
||||
: IsColumnsNullable<Table, Rest extends (keyof Table['Row'])[] ? Rest : []>
|
||||
: false
|
||||
: false
|
||||
|
||||
// Check weither or not a 1-1 relation is nullable by checking against the type of the columns
|
||||
export type IsRelationNullable<
|
||||
Table extends GenericTable,
|
||||
Relation extends GenericRelationship,
|
||||
> = IsColumnsNullable<Table, Relation['columns']>
|
||||
|
||||
type TableForwardRelationships<
|
||||
Schema extends GenericSchema,
|
||||
TName,
|
||||
> = TName extends keyof TablesAndViews<Schema>
|
||||
? UnionToArray<
|
||||
RecursivelyFindRelationships<Schema, TName, keyof TablesAndViews<Schema>>
|
||||
> extends infer R
|
||||
? R extends (GenericRelationship & { from: keyof TablesAndViews<Schema> })[]
|
||||
? R
|
||||
: []
|
||||
: []
|
||||
: []
|
||||
|
||||
type RecursivelyFindRelationships<
|
||||
Schema extends GenericSchema,
|
||||
TName,
|
||||
Keys extends keyof TablesAndViews<Schema>,
|
||||
> = Keys extends infer K
|
||||
? K extends keyof TablesAndViews<Schema>
|
||||
? FilterRelationships<TablesAndViews<Schema>[K]['Relationships'], TName, K> extends never
|
||||
? RecursivelyFindRelationships<Schema, TName, Exclude<Keys, K>>
|
||||
:
|
||||
| FilterRelationships<TablesAndViews<Schema>[K]['Relationships'], TName, K>
|
||||
| RecursivelyFindRelationships<Schema, TName, Exclude<Keys, K>>
|
||||
: false
|
||||
: false
|
||||
|
||||
type FilterRelationships<R, TName, From> = R extends readonly (infer Rel)[]
|
||||
? Rel extends { referencedRelation: TName }
|
||||
? Rel & { from: From }
|
||||
: never
|
||||
: never
|
||||
|
||||
export type ResolveForwardRelationship<
|
||||
Schema extends GenericSchema,
|
||||
Field extends Ast.FieldNode,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
> =
|
||||
FindFieldMatchingRelationships<
|
||||
Schema,
|
||||
TablesAndViews<Schema>[Field['name']]['Relationships'],
|
||||
Ast.FieldNode & { name: CurrentTableOrView; hint: Field['hint'] }
|
||||
> extends infer FoundByName
|
||||
? FoundByName extends GenericRelationship
|
||||
? {
|
||||
referencedTable: TablesAndViews<Schema>[Field['name']]
|
||||
relation: FoundByName
|
||||
direction: 'forward'
|
||||
from: Field['name']
|
||||
type: 'found-by-name'
|
||||
}
|
||||
: FindFieldMatchingRelationships<
|
||||
Schema,
|
||||
TableForwardRelationships<Schema, CurrentTableOrView>,
|
||||
Field
|
||||
> extends infer FoundByMatch
|
||||
? FoundByMatch extends GenericRelationship & {
|
||||
from: keyof TablesAndViews<Schema>
|
||||
}
|
||||
? {
|
||||
referencedTable: TablesAndViews<Schema>[FoundByMatch['from']]
|
||||
relation: FoundByMatch
|
||||
direction: 'forward'
|
||||
from: CurrentTableOrView
|
||||
type: 'found-by-match'
|
||||
}
|
||||
: FindJoinTableRelationship<
|
||||
Schema,
|
||||
CurrentTableOrView,
|
||||
Field['name']
|
||||
> extends infer FoundByJoinTable
|
||||
? FoundByJoinTable extends GenericRelationship
|
||||
? {
|
||||
referencedTable: TablesAndViews<Schema>[FoundByJoinTable['referencedRelation']]
|
||||
relation: FoundByJoinTable & { match: 'refrel' }
|
||||
direction: 'forward'
|
||||
from: CurrentTableOrView
|
||||
type: 'found-by-join-table'
|
||||
}
|
||||
: ResolveEmbededFunctionJoinTableRelationship<
|
||||
Schema,
|
||||
CurrentTableOrView,
|
||||
Field['name']
|
||||
> extends infer FoundEmbededFunctionJoinTableRelation
|
||||
? FoundEmbededFunctionJoinTableRelation extends GenericFunction
|
||||
? FoundEmbededFunctionJoinTableRelation['SetofOptions'] extends GenericSetofOption
|
||||
? FoundEmbededFunctionJoinTableRelation['SetofOptions']['to'] extends ''
|
||||
? // Scalar computed column: function returns a primitive (not a table row).
|
||||
// `to` is '' because there is no target table — the value is returned directly.
|
||||
{
|
||||
referencedTable: { Row: Record<string, never>; Relationships: [] }
|
||||
relation: {
|
||||
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_scalar_forward`
|
||||
columns: []
|
||||
isOneToOne: false
|
||||
referencedColumns: []
|
||||
referencedRelation: ''
|
||||
} & {
|
||||
match: 'func'
|
||||
isNotNullable: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isNotNullable'] extends true
|
||||
? true
|
||||
: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn'] extends true
|
||||
? false
|
||||
: true
|
||||
isSetofReturn: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn']
|
||||
}
|
||||
scalarType: FoundEmbededFunctionJoinTableRelation['Returns']
|
||||
direction: 'forward'
|
||||
from: CurrentTableOrView
|
||||
type: 'found-by-embeded-scalar-function'
|
||||
}
|
||||
: // Table-valued function: `to` names the target table/view.
|
||||
{
|
||||
referencedTable: TablesAndViews<Schema>[FoundEmbededFunctionJoinTableRelation['SetofOptions']['to']]
|
||||
relation: {
|
||||
foreignKeyName: `${Field['name']}_${CurrentTableOrView}_${FoundEmbededFunctionJoinTableRelation['SetofOptions']['to']}_forward`
|
||||
columns: []
|
||||
isOneToOne: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isOneToOne'] extends true
|
||||
? true
|
||||
: false
|
||||
referencedColumns: []
|
||||
referencedRelation: FoundEmbededFunctionJoinTableRelation['SetofOptions']['to']
|
||||
} & {
|
||||
match: 'func'
|
||||
isNotNullable: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isNotNullable'] extends true
|
||||
? true
|
||||
: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn'] extends true
|
||||
? false
|
||||
: true
|
||||
isSetofReturn: FoundEmbededFunctionJoinTableRelation['SetofOptions']['isSetofReturn']
|
||||
}
|
||||
direction: 'forward'
|
||||
from: CurrentTableOrView
|
||||
type: 'found-by-embeded-function'
|
||||
}
|
||||
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
|
||||
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
|
||||
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
|
||||
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
|
||||
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
|
||||
: SelectQueryError<`could not find the relation between ${CurrentTableOrView} and ${Field['name']}`>
|
||||
|
||||
/**
|
||||
* Given a CurrentTableOrView, finds all join tables to this relation.
|
||||
* For example, if products and categories are linked via product_categories table:
|
||||
*
|
||||
* @example Find join table relationship
|
||||
* Given:
|
||||
* - CurrentTableView = 'products'
|
||||
* - FieldName = "categories"
|
||||
*
|
||||
* It should return this relationship from product_categories:
|
||||
* {
|
||||
* foreignKeyName: "product_categories_category_id_fkey",
|
||||
* columns: ["category_id"],
|
||||
* isOneToOne: false,
|
||||
* referencedRelation: "categories",
|
||||
* referencedColumns: ["id"]
|
||||
* }
|
||||
*/
|
||||
type ResolveJoinTableRelationship<
|
||||
Schema extends GenericSchema,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
FieldName extends string,
|
||||
> = {
|
||||
[TableName in keyof TablesAndViews<Schema>]: DeduplicateRelationships<
|
||||
TablesAndViews<Schema>[TableName]['Relationships']
|
||||
> extends readonly (infer Rel)[]
|
||||
? Rel extends { referencedRelation: CurrentTableOrView }
|
||||
? DeduplicateRelationships<
|
||||
TablesAndViews<Schema>[TableName]['Relationships']
|
||||
> extends readonly (infer OtherRel)[]
|
||||
? OtherRel extends { referencedRelation: FieldName }
|
||||
? OtherRel
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
}[keyof TablesAndViews<Schema>]
|
||||
|
||||
type ResolveEmbededFunctionJoinTableRelationship<
|
||||
Schema extends GenericSchema,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
FieldName extends string,
|
||||
> =
|
||||
FindMatchingFunctionBySetofFrom<
|
||||
Schema['Functions'][FieldName],
|
||||
CurrentTableOrView
|
||||
> extends infer Fn
|
||||
? Fn extends GenericFunction
|
||||
? Fn
|
||||
: false
|
||||
: false
|
||||
|
||||
export type FindJoinTableRelationship<
|
||||
Schema extends GenericSchema,
|
||||
CurrentTableOrView extends keyof TablesAndViews<Schema> & string,
|
||||
FieldName extends string,
|
||||
> =
|
||||
ResolveJoinTableRelationship<Schema, CurrentTableOrView, FieldName> extends infer Result
|
||||
? [Result] extends [never]
|
||||
? false
|
||||
: Result
|
||||
: never
|
||||
/**
|
||||
* Finds a matching relationship based on the FieldNode's name and optional hint.
|
||||
*/
|
||||
export type FindFieldMatchingRelationships<
|
||||
Schema extends GenericSchema,
|
||||
Relationships extends GenericRelationship[],
|
||||
Field extends Ast.FieldNode,
|
||||
> = Field extends { hint: string }
|
||||
? FindMatchingHintTableRelationships<
|
||||
Schema,
|
||||
Relationships,
|
||||
Field['hint'],
|
||||
Field['name']
|
||||
> extends GenericRelationship
|
||||
? FindMatchingHintTableRelationships<Schema, Relationships, Field['hint'], Field['name']> & {
|
||||
branch: 'found-in-table-via-hint'
|
||||
hint: Field['hint']
|
||||
}
|
||||
: FindMatchingHintViewRelationships<
|
||||
Schema,
|
||||
Relationships,
|
||||
Field['hint'],
|
||||
Field['name']
|
||||
> extends GenericRelationship
|
||||
? FindMatchingHintViewRelationships<Schema, Relationships, Field['hint'], Field['name']> & {
|
||||
branch: 'found-in-view-via-hint'
|
||||
hint: Field['hint']
|
||||
}
|
||||
: SelectQueryError<'Failed to find matching relation via hint'>
|
||||
: FindMatchingTableRelationships<Schema, Relationships, Field['name']> extends GenericRelationship
|
||||
? FindMatchingTableRelationships<Schema, Relationships, Field['name']> & {
|
||||
branch: 'found-in-table-via-name'
|
||||
name: Field['name']
|
||||
}
|
||||
: FindMatchingViewRelationships<
|
||||
Schema,
|
||||
Relationships,
|
||||
Field['name']
|
||||
> extends GenericRelationship
|
||||
? FindMatchingViewRelationships<Schema, Relationships, Field['name']> & {
|
||||
branch: 'found-in-view-via-name'
|
||||
name: Field['name']
|
||||
}
|
||||
: SelectQueryError<'Failed to find matching relation via name'>
|
||||
|
||||
export type JsonPathToAccessor<Path extends string> = Path extends `${infer P1}->${infer P2}`
|
||||
? P2 extends `>${infer Rest}` // Handle ->> operator
|
||||
? JsonPathToAccessor<`${P1}.${Rest}`>
|
||||
: P2 extends string // Handle -> operator
|
||||
? JsonPathToAccessor<`${P1}.${P2}`>
|
||||
: Path
|
||||
: Path extends `>${infer Rest}` // Clean up any remaining > characters
|
||||
? JsonPathToAccessor<Rest>
|
||||
: Path extends `${infer P1}::${infer _}` // Handle type casting
|
||||
? JsonPathToAccessor<P1>
|
||||
: Path extends `${infer P1}${')' | ','}${infer _}` // Handle closing parenthesis and comma
|
||||
? P1
|
||||
: Path
|
||||
|
||||
export type JsonPathToType<T, Path extends string> = Path extends ''
|
||||
? T
|
||||
: ContainsNull<T> extends true
|
||||
? JsonPathToType<Exclude<T, null>, Path>
|
||||
: Path extends `${infer Key}.${infer Rest}`
|
||||
? Key extends keyof T
|
||||
? JsonPathToType<T[Key], Rest>
|
||||
: never
|
||||
: Path extends keyof T
|
||||
? T[Path]
|
||||
: never
|
||||
|
||||
export type IsStringUnion<T> = string extends T
|
||||
? false
|
||||
: T extends string
|
||||
? [T] extends [never]
|
||||
? false
|
||||
: true
|
||||
: false
|
||||
|
||||
type MatchingFunctionBySetofFrom<
|
||||
Fn extends GenericFunction,
|
||||
TableName extends string,
|
||||
> = Fn['SetofOptions'] extends GenericSetofOption
|
||||
? TableName extends Fn['SetofOptions']['from']
|
||||
? Fn
|
||||
: never
|
||||
: false
|
||||
|
||||
type FindMatchingFunctionBySetofFrom<
|
||||
FnUnion,
|
||||
TableName extends string,
|
||||
> = FnUnion extends infer Fn extends GenericFunction
|
||||
? MatchingFunctionBySetofFrom<Fn, TableName>
|
||||
: false
|
||||
|
||||
type ComputedField<
|
||||
Schema extends GenericSchema,
|
||||
RelationName extends keyof TablesAndViews<Schema>,
|
||||
FieldName extends keyof TablesAndViews<Schema>[RelationName]['Row'],
|
||||
> = FieldName extends keyof Schema['Functions']
|
||||
? [Schema['Functions'][FieldName]['Args']] extends [never]
|
||||
? never
|
||||
: Schema['Functions'][FieldName] extends {
|
||||
Args: { '': TablesAndViews<Schema>[RelationName]['Row'] }
|
||||
Returns: any
|
||||
}
|
||||
? FieldName
|
||||
: never
|
||||
: never
|
||||
|
||||
// Given a relation name (Table or View) extract all the "computed fields" based on the Row
|
||||
// object, and the schema functions definitions
|
||||
export type GetComputedFields<
|
||||
Schema extends GenericSchema,
|
||||
RelationName extends keyof TablesAndViews<Schema>,
|
||||
> = {
|
||||
[K in keyof TablesAndViews<Schema>[RelationName]['Row']]: ComputedField<Schema, RelationName, K>
|
||||
}[keyof TablesAndViews<Schema>[RelationName]['Row']]
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Types that are shared between supabase-js and postgrest-js
|
||||
|
||||
export type Fetch = typeof fetch
|
||||
|
||||
/**
|
||||
* Default number of retry attempts.
|
||||
*/
|
||||
export const DEFAULT_MAX_RETRIES = 3
|
||||
|
||||
/**
|
||||
* Default exponential backoff delay function.
|
||||
* Delays: 1s, 2s, 4s, 8s, ... (max 30s)
|
||||
*
|
||||
* @param attemptIndex - Zero-based index of the retry attempt
|
||||
* @returns Delay in milliseconds before the next retry
|
||||
*/
|
||||
export const getRetryDelay = (attemptIndex: number): number =>
|
||||
Math.min(1000 * 2 ** attemptIndex, 30000)
|
||||
|
||||
/**
|
||||
* Status codes that are safe to retry.
|
||||
* 520 = Cloudflare timeout/connection errors (transient)
|
||||
* 503 = PostgREST schema cache not yet loaded (transient, signals retry via Retry-After header)
|
||||
*/
|
||||
export const RETRYABLE_STATUS_CODES = [520, 503] as const
|
||||
|
||||
/**
|
||||
* HTTP methods that are safe to retry (idempotent operations).
|
||||
*/
|
||||
export const RETRYABLE_METHODS = ['GET', 'HEAD', 'OPTIONS'] as const
|
||||
|
||||
export type GenericRelationship = {
|
||||
foreignKeyName: string
|
||||
columns: string[]
|
||||
isOneToOne?: boolean
|
||||
referencedRelation: string
|
||||
referencedColumns: string[]
|
||||
}
|
||||
|
||||
export type GenericTable = {
|
||||
Row: Record<string, unknown>
|
||||
Insert: Record<string, unknown>
|
||||
Update: Record<string, unknown>
|
||||
Relationships: GenericRelationship[]
|
||||
}
|
||||
|
||||
export type GenericUpdatableView = {
|
||||
Row: Record<string, unknown>
|
||||
Insert: Record<string, unknown>
|
||||
Update: Record<string, unknown>
|
||||
Relationships: GenericRelationship[]
|
||||
}
|
||||
|
||||
export type GenericNonUpdatableView = {
|
||||
Row: Record<string, unknown>
|
||||
Relationships: GenericRelationship[]
|
||||
}
|
||||
|
||||
export type GenericView = GenericUpdatableView | GenericNonUpdatableView
|
||||
|
||||
export type GenericSetofOption = {
|
||||
isSetofReturn?: boolean | undefined
|
||||
isOneToOne?: boolean | undefined
|
||||
isNotNullable?: boolean | undefined
|
||||
to: string
|
||||
from: string
|
||||
}
|
||||
|
||||
export type GenericFunction = {
|
||||
Args: Record<string, unknown> | never
|
||||
Returns: unknown
|
||||
SetofOptions?: GenericSetofOption
|
||||
}
|
||||
|
||||
export type GenericSchema = {
|
||||
Tables: Record<string, GenericTable>
|
||||
Views: Record<string, GenericView>
|
||||
Functions: Record<string, GenericFunction>
|
||||
}
|
||||
|
||||
export type ClientServerOptions = {
|
||||
PostgrestVersion?: string
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import type { GenericFunction, GenericSchema, GenericSetofOption } from './common'
|
||||
|
||||
// Functions matching utils
|
||||
type IsMatchingArgs<
|
||||
FnArgs extends GenericFunction['Args'],
|
||||
PassedArgs extends GenericFunction['Args'],
|
||||
> = [FnArgs] extends [Record<PropertyKey, never>]
|
||||
? PassedArgs extends Record<PropertyKey, never>
|
||||
? true
|
||||
: false
|
||||
: keyof PassedArgs extends keyof FnArgs
|
||||
? PassedArgs extends FnArgs
|
||||
? true
|
||||
: false
|
||||
: false
|
||||
|
||||
type MatchingFunctionArgs<
|
||||
Fn extends GenericFunction,
|
||||
Args extends GenericFunction['Args'],
|
||||
> = Fn extends { Args: infer A extends GenericFunction['Args'] }
|
||||
? IsMatchingArgs<A, Args> extends true
|
||||
? Fn
|
||||
: never
|
||||
: false
|
||||
|
||||
type FindMatchingFunctionByArgs<
|
||||
FnUnion,
|
||||
Args extends GenericFunction['Args'],
|
||||
> = FnUnion extends infer Fn extends GenericFunction ? MatchingFunctionArgs<Fn, Args> : false
|
||||
|
||||
// Types for working with database schemas
|
||||
type TablesAndViews<Schema extends GenericSchema> = Schema['Tables'] & Exclude<Schema['Views'], ''>
|
||||
|
||||
// Utility types for working with unions
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
type LastOf<T> =
|
||||
UnionToIntersection<T extends any ? () => T : never> extends () => infer R ? R : never
|
||||
|
||||
type IsAny<T> = 0 extends 1 & T ? true : false
|
||||
|
||||
type ExactMatch<T, S> = [T] extends [S] ? ([S] extends [T] ? true : false) : false
|
||||
|
||||
type ExtractExactFunction<Fns, Args> = Fns extends infer F
|
||||
? F extends GenericFunction
|
||||
? ExactMatch<F['Args'], Args> extends true
|
||||
? F
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
|
||||
type IsNever<T> = [T] extends [never] ? true : false
|
||||
|
||||
type RpcFunctionNotFound<FnName> = {
|
||||
Row: any
|
||||
Result: {
|
||||
error: true
|
||||
} & "Couldn't infer function definition matching provided arguments"
|
||||
RelationName: FnName
|
||||
Relationships: null
|
||||
}
|
||||
|
||||
type CrossSchemaError<TableRef extends string> = {
|
||||
error: true
|
||||
} & `Function returns SETOF from a different schema ('${TableRef}'). Use .overrideTypes<YourReturnType>() to specify the return type explicitly.`
|
||||
|
||||
export type GetRpcFunctionFilterBuilderByArgs<
|
||||
Schema extends GenericSchema,
|
||||
FnName extends string & keyof Schema['Functions'],
|
||||
Args,
|
||||
> = {
|
||||
0: Schema['Functions'][FnName]
|
||||
// If the Args is exactly never (function call without any params)
|
||||
1: IsAny<Schema> extends true
|
||||
? any
|
||||
: IsNever<Args> extends true
|
||||
? // This is for retro compatibility, if the funcition is defined with an single return and an union of Args
|
||||
// we fallback to the last function definition matched by name
|
||||
IsNever<ExtractExactFunction<Schema['Functions'][FnName], Args>> extends true
|
||||
? LastOf<Schema['Functions'][FnName]>
|
||||
: ExtractExactFunction<Schema['Functions'][FnName], Args>
|
||||
: Args extends Record<PropertyKey, never>
|
||||
? LastOf<Schema['Functions'][FnName]>
|
||||
: // Otherwise, we attempt to match with one of the function definition in the union based
|
||||
// on the function arguments provided
|
||||
Args extends GenericFunction['Args']
|
||||
? // This is for retro compatibility, if the funcition is defined with an single return and an union of Args
|
||||
// we fallback to the last function definition matched by name
|
||||
IsNever<
|
||||
LastOf<FindMatchingFunctionByArgs<Schema['Functions'][FnName], Args>>
|
||||
> extends true
|
||||
? LastOf<Schema['Functions'][FnName]>
|
||||
: // Otherwise, we use the arguments based function definition narrowing to get the right value
|
||||
LastOf<FindMatchingFunctionByArgs<Schema['Functions'][FnName], Args>>
|
||||
: // If we can't find a matching function by args, we try to find one by function name
|
||||
ExtractExactFunction<Schema['Functions'][FnName], Args> extends GenericFunction
|
||||
? ExtractExactFunction<Schema['Functions'][FnName], Args>
|
||||
: any
|
||||
}[1] extends infer Fn
|
||||
? // If we are dealing with an non-typed client everything is any
|
||||
IsAny<Fn> extends true
|
||||
? { Row: any; Result: any; RelationName: FnName; Relationships: null }
|
||||
: // Otherwise, we use the arguments based function definition narrowing to get the right value
|
||||
Fn extends GenericFunction
|
||||
? {
|
||||
Row: Fn['SetofOptions'] extends GenericSetofOption
|
||||
? Fn['SetofOptions']['to'] extends keyof TablesAndViews<Schema>
|
||||
? TablesAndViews<Schema>[Fn['SetofOptions']['to']]['Row']
|
||||
: // Cross-schema fallback: use Returns type when table is not in current schema
|
||||
Fn['Returns'] extends any[]
|
||||
? Fn['Returns'][number] extends Record<string, unknown>
|
||||
? Fn['Returns'][number]
|
||||
: CrossSchemaError<Fn['SetofOptions']['to'] & string>
|
||||
: Fn['Returns'] extends Record<string, unknown>
|
||||
? Fn['Returns']
|
||||
: CrossSchemaError<Fn['SetofOptions']['to'] & string>
|
||||
: Fn['Returns'] extends any[]
|
||||
? Fn['Returns'][number] extends Record<string, unknown>
|
||||
? Fn['Returns'][number]
|
||||
: never
|
||||
: Fn['Returns'] extends Record<string, unknown>
|
||||
? Fn['Returns']
|
||||
: never
|
||||
Result: Fn['SetofOptions'] extends GenericSetofOption
|
||||
? Fn['SetofOptions']['isSetofReturn'] extends true
|
||||
? Fn['SetofOptions']['isOneToOne'] extends true
|
||||
? Fn['Returns'][]
|
||||
: Fn['Returns']
|
||||
: Fn['Returns']
|
||||
: Fn['Returns']
|
||||
RelationName: Fn['SetofOptions'] extends GenericSetofOption
|
||||
? Fn['SetofOptions']['to']
|
||||
: FnName
|
||||
Relationships: Fn['SetofOptions'] extends GenericSetofOption
|
||||
? Fn['SetofOptions']['to'] extends keyof Schema['Tables']
|
||||
? Schema['Tables'][Fn['SetofOptions']['to']]['Relationships']
|
||||
: Fn['SetofOptions']['to'] extends keyof Schema['Views']
|
||||
? Schema['Views'][Fn['SetofOptions']['to']]['Relationships']
|
||||
: null
|
||||
: null
|
||||
}
|
||||
: // If we failed to find the function by argument, we still pass with any but also add an overridable
|
||||
Fn extends false
|
||||
? RpcFunctionNotFound<FnName>
|
||||
: RpcFunctionNotFound<FnName>
|
||||
: RpcFunctionNotFound<FnName>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
type IsPostgrest13<PostgrestVersion extends string | undefined> =
|
||||
PostgrestVersion extends `13${string}` ? true : false
|
||||
type IsPostgrest14<PostgrestVersion extends string | undefined> =
|
||||
PostgrestVersion extends `14${string}` ? true : false
|
||||
|
||||
type IsPostgrestVersionGreaterThan12<PostgrestVersion extends string | undefined> =
|
||||
IsPostgrest13<PostgrestVersion> extends true
|
||||
? true
|
||||
: IsPostgrest14<PostgrestVersion> extends true
|
||||
? true
|
||||
: false
|
||||
|
||||
export type MaxAffectedEnabled<PostgrestVersion extends string | undefined> =
|
||||
IsPostgrestVersionGreaterThan12<PostgrestVersion> extends true ? true : false
|
||||
|
||||
export type SpreadOnManyEnabled<PostgrestVersion extends string | undefined> =
|
||||
IsPostgrestVersionGreaterThan12<PostgrestVersion> extends true ? true : false
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import PostgrestError from '../PostgrestError'
|
||||
import { ContainsNull } from '../select-query-parser/types'
|
||||
import { SelectQueryError } from '../select-query-parser/utils'
|
||||
import { ClientServerOptions } from './common/common'
|
||||
|
||||
/**
|
||||
* Response format
|
||||
*
|
||||
* {@link https://github.com/supabase/supabase-js/issues/32}
|
||||
*/
|
||||
interface PostgrestResponseBase {
|
||||
status: number
|
||||
statusText: string
|
||||
}
|
||||
export interface PostgrestResponseSuccess<T> extends PostgrestResponseBase {
|
||||
success: true
|
||||
error: null
|
||||
data: T
|
||||
count: number | null
|
||||
}
|
||||
export interface PostgrestResponseFailure extends PostgrestResponseBase {
|
||||
success: false
|
||||
error: PostgrestError
|
||||
data: null
|
||||
count: null
|
||||
}
|
||||
|
||||
// TODO: in v3:
|
||||
// - remove PostgrestResponse and PostgrestMaybeSingleResponse
|
||||
// - rename PostgrestSingleResponse to PostgrestResponse
|
||||
export type PostgrestSingleResponse<T> = PostgrestResponseSuccess<T> | PostgrestResponseFailure
|
||||
export type PostgrestMaybeSingleResponse<T> = PostgrestSingleResponse<T | null>
|
||||
export type PostgrestResponse<T> = PostgrestSingleResponse<T[]>
|
||||
|
||||
export type DatabaseWithOptions<Database, Options extends ClientServerOptions> = {
|
||||
db: Database
|
||||
options: Options
|
||||
}
|
||||
|
||||
// https://twitter.com/mattpocockuk/status/1622730173446557697
|
||||
export type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
||||
|
||||
// Rejects excess properties that aren't in Base.
|
||||
// Works around TypeScript not checking excess properties on generic parameters.
|
||||
export type RejectExcessProperties<Base, Row> = Row & {
|
||||
[K in Exclude<keyof Row, keyof Base>]: never
|
||||
}
|
||||
|
||||
// https://github.com/sindresorhus/type-fest
|
||||
export type SimplifyDeep<Type, ExcludeType = never> = ConditionalSimplifyDeep<
|
||||
Type,
|
||||
ExcludeType | NonRecursiveType | Set<unknown> | Map<unknown, unknown>,
|
||||
object
|
||||
>
|
||||
type ConditionalSimplifyDeep<
|
||||
Type,
|
||||
ExcludeType = never,
|
||||
IncludeType = unknown,
|
||||
> = Type extends ExcludeType
|
||||
? Type
|
||||
: Type extends IncludeType
|
||||
? { [TypeKey in keyof Type]: ConditionalSimplifyDeep<Type[TypeKey], ExcludeType, IncludeType> }
|
||||
: Type
|
||||
type NonRecursiveType = BuiltIns | Function | (new (...arguments_: any[]) => unknown)
|
||||
type BuiltIns = Primitive | void | Date | RegExp
|
||||
type Primitive = null | undefined | string | number | boolean | symbol | bigint
|
||||
|
||||
export type IsValidResultOverride<Result, NewResult, ErrorResult, ErrorNewResult> =
|
||||
Result extends any[]
|
||||
? NewResult extends any[]
|
||||
? // Both are arrays - valid
|
||||
true
|
||||
: ErrorResult
|
||||
: NewResult extends any[]
|
||||
? ErrorNewResult
|
||||
: // Neither are arrays - valid
|
||||
true
|
||||
/**
|
||||
* Utility type to check if array types match between Result and NewResult.
|
||||
* Returns either the valid NewResult type or an error message type.
|
||||
*/
|
||||
export type CheckMatchingArrayTypes<Result, NewResult> =
|
||||
// If the result is a QueryError we allow the user to override anyway
|
||||
Result extends SelectQueryError<string>
|
||||
? NewResult
|
||||
: IsValidResultOverride<
|
||||
Result,
|
||||
NewResult,
|
||||
{
|
||||
Error: 'Type mismatch: Cannot cast array result to a single object. Use .overrideTypes<Array<YourType>> or .returns<Array<YourType>> (deprecated) for array results or .single() to convert the result to a single object'
|
||||
},
|
||||
{
|
||||
Error: 'Type mismatch: Cannot cast single object to array type. Remove Array wrapper from return type or make sure you are not using .single() up in the calling chain'
|
||||
}
|
||||
> extends infer ValidationResult
|
||||
? ValidationResult extends true
|
||||
? // Preserve the optionality of the result if the overriden type is an object (case of chaining with `maybeSingle`)
|
||||
ContainsNull<Result> extends true
|
||||
? NewResult | null
|
||||
: NewResult
|
||||
: // contains the error
|
||||
ValidationResult
|
||||
: never
|
||||
|
||||
type Simplify<T> = T extends object ? { [K in keyof T]: T[K] } : T
|
||||
|
||||
// Extract only explicit (non-index-signature) keys.
|
||||
type ExplicitKeys<T> = {
|
||||
[K in keyof T]: string extends K ? never : K
|
||||
}[keyof T]
|
||||
|
||||
type MergeExplicit<New, Row> = {
|
||||
// We merge all the explicit keys which allows merge and override of types like
|
||||
// { [key: string]: unknown } and { someSpecificKey: boolean }
|
||||
[K in ExplicitKeys<New> | ExplicitKeys<Row>]: K extends keyof New
|
||||
? K extends keyof Row
|
||||
? Row[K] extends SelectQueryError<string>
|
||||
? New[K]
|
||||
: // Check if the override is on a embedded relation (array)
|
||||
New[K] extends any[]
|
||||
? Row[K] extends any[]
|
||||
? Array<Simplify<MergeDeep<NonNullable<New[K][number]>, NonNullable<Row[K][number]>>>>
|
||||
: New[K]
|
||||
: // Check if both properties are objects omitting a potential null union
|
||||
IsPlainObject<NonNullable<New[K]>> extends true
|
||||
? IsPlainObject<NonNullable<Row[K]>> extends true
|
||||
? // If they are, use the new override as source of truth for the optionality
|
||||
ContainsNull<New[K]> extends true
|
||||
? // If the override wants to preserve optionality
|
||||
Simplify<MergeDeep<NonNullable<New[K]>, NonNullable<Row[K]>>> | null
|
||||
: // If the override wants to enforce non-null result
|
||||
Simplify<MergeDeep<New[K], NonNullable<Row[K]>>>
|
||||
: New[K] // Override with New type if Row isn't an object
|
||||
: New[K] // Override primitives with New type
|
||||
: New[K] // Add new properties from New
|
||||
: K extends keyof Row
|
||||
? Row[K] // Keep existing properties not in New
|
||||
: never
|
||||
}
|
||||
|
||||
type MergeDeep<New, Row> = Simplify<
|
||||
MergeExplicit<New, Row> &
|
||||
// Intersection here is to restore dynamic keys into the merging result
|
||||
// eg:
|
||||
// {[key: number]: string}
|
||||
// or Record<string, number | null>
|
||||
(string extends keyof Row ? { [K: string]: Row[string] } : {})
|
||||
>
|
||||
|
||||
// Helper to check if a type is a plain object (not an array)
|
||||
type IsPlainObject<T> = T extends any[] ? false : T extends object ? true : false
|
||||
|
||||
// Merge the new result with the original (Result) when merge option is true.
|
||||
// If NewResult is an array, merge each element.
|
||||
export type MergePartialResult<NewResult, Result, Options> = Options extends { merge: true }
|
||||
? Result extends any[]
|
||||
? NewResult extends any[]
|
||||
? Array<Simplify<MergeDeep<NewResult[number], Result[number]>>>
|
||||
: never
|
||||
: Simplify<MergeDeep<NewResult, Result>>
|
||||
: NewResult
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Generated automatically during releases by scripts/update-version-files.ts
|
||||
// This file provides runtime access to the package version for:
|
||||
// - HTTP request headers (e.g., X-Client-Info header for API requests)
|
||||
// - Debugging and support (identifying which version is running)
|
||||
// - Telemetry and logging (version reporting in errors/analytics)
|
||||
// - Ensuring build artifacts match the published package version
|
||||
export const version = '2.110.8'
|
||||
Reference in New Issue
Block a user