@palbase/backend 25.1.0 → 27.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/palbase-backend.cjs +2401 -1039
- package/dist/bin/palbase-backend.cjs.map +1 -1
- package/dist/bin/palbase-backend.js +87 -51
- package/dist/bin/palbase-backend.js.map +1 -1
- package/dist/chunk-CGNN2PUH.js +213 -0
- package/dist/chunk-CGNN2PUH.js.map +1 -0
- package/dist/chunk-DRZFQRJI.js +370 -0
- package/dist/chunk-DRZFQRJI.js.map +1 -0
- package/dist/chunk-GYK6QYS4.js +708 -0
- package/dist/chunk-GYK6QYS4.js.map +1 -0
- package/dist/chunk-I3C4PFIW.js +930 -0
- package/dist/chunk-I3C4PFIW.js.map +1 -0
- package/dist/{chunk-VDF2T4AS.js → chunk-OO7R25AI.js} +1213 -591
- package/dist/chunk-OO7R25AI.js.map +1 -0
- package/dist/chunk-TS4U7NBD.js +885 -0
- package/dist/chunk-TS4U7NBD.js.map +1 -0
- package/dist/{chunk-YOY5DFQS.js → chunk-TWX6JTGJ.js} +76 -34
- package/dist/{chunk-YOY5DFQS.js.map → chunk-TWX6JTGJ.js.map} +1 -1
- package/dist/{chunk-35PNTIRN.js → chunk-VVMJEVQP.js} +63 -162
- package/dist/chunk-VVMJEVQP.js.map +1 -0
- package/dist/{chunk-7D4SUZUM.js → chunk-VXPNPVAG.js} +3 -1
- package/dist/db/env.cjs.map +1 -1
- package/dist/db/env.d.cts +2 -2
- package/dist/db/env.d.ts +2 -2
- package/dist/db/index.cjs +754 -344
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +7 -4
- package/dist/engine/index.cjs +2339 -1006
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.d.cts +6 -6
- package/dist/engine/index.d.ts +6 -6
- package/dist/engine/index.js +7 -6
- package/dist/index-BrvvxSpn.d.ts +4844 -0
- package/dist/index-Bve7BBTL.d.cts +4844 -0
- package/dist/{index-CUomTA3e.d.ts → index-NuzRCuxe.d.ts} +171 -296
- package/dist/{index-ClpDeSos.d.cts → index-VtToZmUm.d.cts} +171 -296
- package/dist/index.cjs +2694 -1169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -19
- package/dist/index.d.ts +165 -19
- package/dist/index.js +738 -477
- package/dist/index.js.map +1 -1
- package/dist/module-Dl1KFVtc.d.cts +54 -0
- package/dist/module-Dl1KFVtc.d.ts +54 -0
- package/dist/openapi/index.cjs +1330 -484
- package/dist/openapi/index.cjs.map +1 -1
- package/dist/openapi/index.d.cts +4 -2
- package/dist/openapi/index.d.ts +4 -2
- package/dist/openapi/index.js +1264 -474
- package/dist/openapi/index.js.map +1 -1
- package/dist/{registry-dZZ5JKYg.d.ts → registry-B0eyOF9x.d.ts} +1 -1
- package/dist/{registry-CC0WBQq6.d.cts → registry-Bk9_rbNd.d.cts} +1 -1
- package/dist/stack.cjs.map +1 -1
- package/dist/test/index.cjs +705 -141
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +30 -4
- package/dist/test/index.d.ts +30 -4
- package/dist/test/index.js +472 -124
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +33 -18
- package/docs/auth.md +1 -1
- package/docs/background.md +2 -2
- package/docs/database.md +221 -50
- package/docs/endpoints.md +3 -4
- package/docs/events.md +3 -3
- package/docs/getting-started.md +1 -1
- package/docs/llms-full.txt +401 -117
- package/docs/migrations.md +2 -2
- package/docs/schema.md +19 -10
- package/docs/services.md +116 -26
- package/package.json +8 -4
- package/stager/generics.js +205 -0
- package/stager/stage.js +39 -3
- package/template/AGENTS.md +110 -72
- package/template/db/public.ts +1 -1
- package/template/{controllers → modules/health}/health.controller.ts +1 -1
- package/template/modules/health/health.module.ts +24 -0
- package/template/modules/notes/note.service.test.ts +49 -0
- package/template/modules/notes/note.service.ts +108 -0
- package/template/{controllers → modules/notes}/notes.controller.ts +17 -11
- package/template/modules/notes/notes.module.ts +37 -0
- package/template/package.json +5 -3
- package/template/scripts/test.sh +33 -0
- package/template/tsconfig.json +29 -30
- package/dist/chunk-35PNTIRN.js.map +0 -1
- package/dist/chunk-CJSKYY76.js +0 -627
- package/dist/chunk-CJSKYY76.js.map +0 -1
- package/dist/chunk-CRQKCRGF.js +0 -276
- package/dist/chunk-CRQKCRGF.js.map +0 -1
- package/dist/chunk-G4R6BTLV.js +0 -662
- package/dist/chunk-G4R6BTLV.js.map +0 -1
- package/dist/chunk-VDF2T4AS.js.map +0 -1
- package/dist/chunk-XABBC7JP.js +0 -55
- package/dist/chunk-XABBC7JP.js.map +0 -1
- package/dist/endpoint-CTEHhb7A.d.ts +0 -2386
- package/dist/endpoint-DYHMo6cC.d.cts +0 -2386
- package/dist/index-CW21M9Z3.d.ts +0 -1222
- package/dist/index-CmBK76nx.d.cts +0 -1222
- package/template/services/note.service.test.ts +0 -45
- package/template/services/note.service.ts +0 -76
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-VXPNPVAG.js.map} +0 -0
- /package/template/{models/notes → modules/notes/dto}/create.ts +0 -0
|
@@ -0,0 +1,4844 @@
|
|
|
1
|
+
import { Tables, TableTypes, Schemas } from './db/env.cjs';
|
|
2
|
+
import { ZodSchema, z } from 'zod';
|
|
3
|
+
import { PalbaseFlagKey, Buckets, BucketTypes, PalbaseSecretName } from './stack.cjs';
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
+
|
|
6
|
+
/** Supported HTTP methods for endpoints. */
|
|
7
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
8
|
+
/**
|
|
9
|
+
* Verified device claim, minted at the platform attestation boundary (App
|
|
10
|
+
* Attest / Play Integrity). Server-owned and immutable: it is populated ONLY
|
|
11
|
+
* from a successful, request-bound verification — never from a raw client
|
|
12
|
+
* header or from user/JWT metadata.
|
|
13
|
+
*/
|
|
14
|
+
interface VerifiedDevice {
|
|
15
|
+
/** Server-minted `auth.installations` anchor (`ins_…`); may be "" until the installation binding is wired at the verify boundary. */
|
|
16
|
+
installationId: string;
|
|
17
|
+
/** Palauth device record (`dev_…`) proven by the App Attest assertion. Absent on the Android integrity path. */
|
|
18
|
+
authDeviceId?: string;
|
|
19
|
+
platform: "ios" | "android" | "web";
|
|
20
|
+
/** Proven verification tier — never a string-matched or header-asserted value. */
|
|
21
|
+
assurance: "attested" | "app_integrity" | "registered";
|
|
22
|
+
}
|
|
23
|
+
/** Authenticated user attached to the request context. */
|
|
24
|
+
interface User {
|
|
25
|
+
id: string;
|
|
26
|
+
/** User's email, if they signed up with one (absent for phone-only users). */
|
|
27
|
+
email?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether that email address has been confirmed.
|
|
30
|
+
*
|
|
31
|
+
* Server-resolved from the verified user profile, not read from the JWT: a
|
|
32
|
+
* token claim is only true as of when the token was minted, so a user who
|
|
33
|
+
* verifies mid-session would keep reporting `false` until it expired.
|
|
34
|
+
*
|
|
35
|
+
* `false` for a phone-only user (no address to confirm) and for any user who
|
|
36
|
+
* has not clicked through yet. Before this field existed, answering "is this
|
|
37
|
+
* address confirmed" in a handler cost an extra network round-trip PER
|
|
38
|
+
* REQUEST for one boolean the runtime already had.
|
|
39
|
+
*/
|
|
40
|
+
emailVerified: boolean;
|
|
41
|
+
role: string;
|
|
42
|
+
metadata: Record<string, unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* Reserved, server-owned verified device claim. `null` on an authenticated
|
|
45
|
+
* request with no verified device. The context assembler is the sole writer;
|
|
46
|
+
* user/JWT metadata can never populate it.
|
|
47
|
+
*/
|
|
48
|
+
device: VerifiedDevice | null;
|
|
49
|
+
}
|
|
50
|
+
/** Authentication configuration for an endpoint. */
|
|
51
|
+
interface AuthConfig {
|
|
52
|
+
/** Whether authentication is required. Defaults to true. */
|
|
53
|
+
required: boolean;
|
|
54
|
+
/** Required role for access. If undefined, any authenticated user is allowed.
|
|
55
|
+
*
|
|
56
|
+
* Matched against the caller's `metadata.role` — NOT `user.role`, which is the
|
|
57
|
+
* database role RLS reads and is always "authenticated" for a signed-in user.
|
|
58
|
+
* Not signed in → 401; signed in with a different or missing role → 403. */
|
|
59
|
+
role?: string;
|
|
60
|
+
/** Require a confirmed email address. An unverified caller gets 403
|
|
61
|
+
* `email_not_verified`. Fences a whole controller; for a partial rule read
|
|
62
|
+
* `user.emailVerified` in the handler instead. */
|
|
63
|
+
verifiedEmail?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Middleware context — subset of EndpointContext without input (not yet validated). */
|
|
67
|
+
interface MiddlewareContext extends PalbaseModuleClients {
|
|
68
|
+
params: Record<string, string>;
|
|
69
|
+
query: Record<string, string>;
|
|
70
|
+
headers: Record<string, string>;
|
|
71
|
+
user: User | null;
|
|
72
|
+
db: DBClient;
|
|
73
|
+
env: Record<string, string>;
|
|
74
|
+
log: Logger;
|
|
75
|
+
cache: CacheClient;
|
|
76
|
+
requestId: string;
|
|
77
|
+
environmentId: string;
|
|
78
|
+
}
|
|
79
|
+
/** Middleware function signature — receives context and next function. */
|
|
80
|
+
type MiddlewareHandler = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* REMOVED IN BEHAVIOUR, KEPT IN NAME.
|
|
83
|
+
*
|
|
84
|
+
* There is no middleware pipeline in this runtime. No bundler reads a
|
|
85
|
+
* `middleware/` directory, the engine never calls a handler defined here, and
|
|
86
|
+
* measured on 2026-08-31 this function had no caller anywhere in the runtime or
|
|
87
|
+
* the CLI. It returned its argument unchanged, so code written against it
|
|
88
|
+
* compiled, deployed, and then never ran — with nothing reporting that.
|
|
89
|
+
*
|
|
90
|
+
* A silent shell is the worst version of a retired feature: it lets a user (or a
|
|
91
|
+
* coding assistant, which is how this surfaced) ship a request logger, an auth
|
|
92
|
+
* check or a rate limiter that simply does not exist in production. So the call
|
|
93
|
+
* refuses, and says where the work belongs.
|
|
94
|
+
*
|
|
95
|
+
* The SYMBOL survives because removing a published export costs a major and
|
|
96
|
+
* 25.0.1 had just shipped. Deleting it is a proposal for the next one; the types
|
|
97
|
+
* below stay either way, so a file that only annotates with them still compiles.
|
|
98
|
+
*
|
|
99
|
+
* This is the shape the SDK already uses for a retired surface: `@Query(schema)`
|
|
100
|
+
* on a parameter throws at decoration time with a message naming its
|
|
101
|
+
* replacement.
|
|
102
|
+
*/
|
|
103
|
+
declare function defineMiddleware(_fn: MiddlewareHandler): never;
|
|
104
|
+
|
|
105
|
+
/** On delete action for foreign key references. */
|
|
106
|
+
type OnDeleteAction = 'cascade' | 'set null' | 'restrict' | 'no action';
|
|
107
|
+
/**
|
|
108
|
+
* The ON DELETE actions permitted on a foreign key to the built-in auth users
|
|
109
|
+
* (`auth.users`). Both let a user's rows be removed (`cascade`) or detached
|
|
110
|
+
* (`set null`) when the account is erased; `restrict` / `no action` would BLOCK
|
|
111
|
+
* erasure and are therefore excluded. This is the CLIENT-SIDE mirror of the
|
|
112
|
+
* server's auth-FK deletion policy — the server (validateAuthUserFK) is the real
|
|
113
|
+
* boundary, this narrows the type so the common mistake is caught at compile time.
|
|
114
|
+
*/
|
|
115
|
+
type AuthUserOnDelete = Extract<OnDeleteAction, 'cascade' | 'set null'>;
|
|
116
|
+
/** Column type identifiers. */
|
|
117
|
+
type ColumnType = 'uuid' | 'text' | 'integer' | 'bigint' | 'numeric' | 'boolean' | 'timestamp' | 'jsonb' | 'enum' | 'vector';
|
|
118
|
+
/** Base column definition shared by all column types. */
|
|
119
|
+
interface ColumnDef {
|
|
120
|
+
type: ColumnType;
|
|
121
|
+
nullable: boolean;
|
|
122
|
+
primaryKey: boolean;
|
|
123
|
+
defaultValue?: unknown;
|
|
124
|
+
defaultRandom?: boolean;
|
|
125
|
+
defaultNow?: boolean;
|
|
126
|
+
references?: {
|
|
127
|
+
table: string;
|
|
128
|
+
column: string;
|
|
129
|
+
};
|
|
130
|
+
/** Pending FK target, resolved by `defineSchema` once every binding exists. */
|
|
131
|
+
referencesThunk?: () => ColumnBuilder;
|
|
132
|
+
/** FK onto THIS table — no thunk needed, the target is the declaring table. */
|
|
133
|
+
selfRefColumn?: string;
|
|
134
|
+
/** This column OWNS the row: erasure, RLS and the `owner` relation read it. */
|
|
135
|
+
owns?: true;
|
|
136
|
+
/** Explicit name for the FORWARD relation (child → parent), when the one
|
|
137
|
+
* derived from the column would be ambiguous or unclear. */
|
|
138
|
+
refAs?: string;
|
|
139
|
+
/**
|
|
140
|
+
* Explicit name for the REVERSE relation (parent → children).
|
|
141
|
+
*
|
|
142
|
+
* Separate from {@link refAs} because the two directions are different facts:
|
|
143
|
+
* the forward name describes the parent this row points at (`author`), the
|
|
144
|
+
* reverse one describes the rows hanging off the parent (`posts`). One option
|
|
145
|
+
* naming both made an ordinary schema undeclarable — `posts.author_id` and
|
|
146
|
+
* `comments.author_id` both named `{ as: "author" }` collided on `users`, and
|
|
147
|
+
* the refusal asked for the `{ as }` they had both already written.
|
|
148
|
+
*/
|
|
149
|
+
reverseAs?: string;
|
|
150
|
+
/** The table this column belongs to; set by `defineTable`. */
|
|
151
|
+
ownerTable?: {
|
|
152
|
+
name: string;
|
|
153
|
+
columns: Record<string, ColumnBuilder>;
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* The name this column used to have. A diff cannot tell a rename from a drop and
|
|
157
|
+
* an add — both leave one name gone and another present — so the intent has to be
|
|
158
|
+
* declared. Without it, renaming a column loses its data.
|
|
159
|
+
*/
|
|
160
|
+
renamedFrom?: string;
|
|
161
|
+
/**
|
|
162
|
+
* This release's PROMISE that it does not reference this column — neither
|
|
163
|
+
* reads it nor writes it, and never names it in a filter, a sort or a SET.
|
|
164
|
+
*
|
|
165
|
+
* The contraction gate reads it and nothing else does: dropping a column is
|
|
166
|
+
* refused while the RUNNING release's declaration still lacks this mark, so
|
|
167
|
+
* removing a column is two deploys — mark it, ship, then drop it.
|
|
168
|
+
*
|
|
169
|
+
* The word is `ignored` and not `deprecated` deliberately: RFC 9745 defines
|
|
170
|
+
* deprecation as changing NO behaviour, and this changes what a deploy will
|
|
171
|
+
* accept. Rails calls the same thing `ignored_columns`.
|
|
172
|
+
*/
|
|
173
|
+
ignored?: boolean;
|
|
174
|
+
onDeleteAction?: OnDeleteAction;
|
|
175
|
+
/** FR-044: yalnız AÇIKÇA `false` bildirilince var. Türev FK index'ini kapatır. */
|
|
176
|
+
index?: boolean;
|
|
177
|
+
/** FR-049: kolon `increment()` ile güncelleniyor. Plan HOT çakışmasını uyarır. */
|
|
178
|
+
counter?: boolean;
|
|
179
|
+
enumName?: string;
|
|
180
|
+
enumValues?: string[];
|
|
181
|
+
unique?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* The value is written by the DATABASE — a trigger, a rule, an identity — not by
|
|
184
|
+
* the author and not by a DEFAULT this schema declares. It makes the column
|
|
185
|
+
* optional on INSERT without putting a DEFAULT in the DDL.
|
|
186
|
+
*
|
|
187
|
+
* Before this existed the only way to keep a trigger-filled column off the
|
|
188
|
+
* INSERT type was to give it a fake `default()`: a value the schema claimed to
|
|
189
|
+
* write and the trigger immediately overwrote. That made the schema lie about
|
|
190
|
+
* its own data.
|
|
191
|
+
*/
|
|
192
|
+
dbAssigned?: boolean;
|
|
193
|
+
/** vector(n): the declared dimension count — part of the TYPE (typmod), read
|
|
194
|
+
* by the wire serializer and the deploy's auto-index (FR-001). */
|
|
195
|
+
dimensions?: number;
|
|
196
|
+
/**
|
|
197
|
+
* How the stored value is projected in and out of this process (FR-009).
|
|
198
|
+
*
|
|
199
|
+
* NOT part of the DDL: the column's Postgres type is unchanged and this pair
|
|
200
|
+
* is never serialized into a migration. It exists so the row surface can hand
|
|
201
|
+
* back the type the application actually works with.
|
|
202
|
+
*/
|
|
203
|
+
transform?: ColumnTransform;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The read/write pair a column may declare (FR-009).
|
|
207
|
+
*
|
|
208
|
+
* `fromDb` takes whatever the driver produced for this column and returns the
|
|
209
|
+
* value the application sees; `toDb` is its inverse on the way out. Kept
|
|
210
|
+
* deliberately unexported — a column declares one inline, nobody needs to name
|
|
211
|
+
* the shape.
|
|
212
|
+
*/
|
|
213
|
+
interface ColumnTransform<T = unknown> {
|
|
214
|
+
fromDb: (value: unknown) => T;
|
|
215
|
+
toDb: (value: T) => unknown;
|
|
216
|
+
}
|
|
217
|
+
declare const __colKind: unique symbol;
|
|
218
|
+
declare const __colNullable: unique symbol;
|
|
219
|
+
declare const __colHasDefault: unique symbol;
|
|
220
|
+
declare const __colEnumValues: unique symbol;
|
|
221
|
+
declare const __colPayload: unique symbol;
|
|
222
|
+
declare const __colTransform: unique symbol;
|
|
223
|
+
/**
|
|
224
|
+
* Fluent column builder with phantom type params:
|
|
225
|
+
* K — ColumnType literal (e.g. "text", "integer")
|
|
226
|
+
* N — boolean: true when nullable() has been called last (false = NOT NULL)
|
|
227
|
+
* D — boolean: true when a default has been set
|
|
228
|
+
* E — enum value union (never for non-enum columns)
|
|
229
|
+
* P — jsonb payload shape (unknown unless jsonb<T>() supplied one)
|
|
230
|
+
* T — transform target type (`never` when the column declares no transform;
|
|
231
|
+
* `never` is the sentinel because it is the only type that survives
|
|
232
|
+
* `[T] extends [never]` and never collides with a real target type)
|
|
233
|
+
*
|
|
234
|
+
* All six params have defaults so bare `ColumnBuilder` (no args) still
|
|
235
|
+
* satisfies `Record<string, ColumnBuilder>` in schema.ts without modification.
|
|
236
|
+
*
|
|
237
|
+
* The six `declare readonly` brand fields carry the phantom types into the
|
|
238
|
+
* structural shape so that conditional types like ColValue<C> can discriminate
|
|
239
|
+
* on K without requiring runtime values on those fields.
|
|
240
|
+
*/
|
|
241
|
+
declare class ColumnBuilder<K extends ColumnType = ColumnType, N extends boolean = boolean, D extends boolean = boolean, E = unknown, P = unknown, T = unknown> {
|
|
242
|
+
readonly [__colKind]: K;
|
|
243
|
+
readonly [__colNullable]: N;
|
|
244
|
+
readonly [__colHasDefault]: D;
|
|
245
|
+
readonly [__colEnumValues]: E;
|
|
246
|
+
readonly [__colPayload]: P;
|
|
247
|
+
readonly [__colTransform]: T;
|
|
248
|
+
readonly _def: ColumnDef;
|
|
249
|
+
constructor(type: K, existingDef?: ColumnDef);
|
|
250
|
+
/** Mark this column as the primary key. */
|
|
251
|
+
primaryKey(): ColumnBuilder<K, N, D, E, P, T>;
|
|
252
|
+
/** Mark this column as NOT NULL (default). */
|
|
253
|
+
notNull(): ColumnBuilder<K, false, D, E, P, T>;
|
|
254
|
+
/** Allow NULL values. */
|
|
255
|
+
nullable(): ColumnBuilder<K, true, D, E, P, T>;
|
|
256
|
+
/** Set a default value. */
|
|
257
|
+
default(value: unknown): ColumnBuilder<K, N, true, E, P, T>;
|
|
258
|
+
/** UUID: generate a random default (gen_random_uuid()). */
|
|
259
|
+
defaultRandom(): ColumnBuilder<K, N, true, E, P, T>;
|
|
260
|
+
/** Timestamp: default to now(). */
|
|
261
|
+
defaultNow(): ColumnBuilder<K, N, true, E, P, T>;
|
|
262
|
+
/**
|
|
263
|
+
* The DATABASE assigns this column's value — a trigger, a rule, an identity.
|
|
264
|
+
*
|
|
265
|
+
* The column becomes optional on INSERT (the author has nothing to send) while
|
|
266
|
+
* the DDL stays free of a DEFAULT this schema would not honour. It is NOT
|
|
267
|
+
* `default()`: that declares a value the schema promises to write.
|
|
268
|
+
*
|
|
269
|
+
* Naming: deliberately not `generated()`. Postgres has GENERATED columns and
|
|
270
|
+
* they are a different thing; borrowing the word would send a reader — or a
|
|
271
|
+
* model writing a schema — to the wrong feature.
|
|
272
|
+
*/
|
|
273
|
+
dbAssigned(): ColumnBuilder<K, N, true, E, P, T>;
|
|
274
|
+
/** Add a foreign key reference. */
|
|
275
|
+
/**
|
|
276
|
+
* Declares that this column used to be called `previous`.
|
|
277
|
+
*
|
|
278
|
+
* A schema diff sees one name gone and another present; it cannot know whether
|
|
279
|
+
* you renamed a column or dropped one and added another, and the two are very
|
|
280
|
+
* different — the second loses every value. Saying so here turns the plan into
|
|
281
|
+
* `ALTER TABLE … RENAME COLUMN` instead.
|
|
282
|
+
*
|
|
283
|
+
* Once the rename has been applied the annotation is inert (the old name is no
|
|
284
|
+
* longer there to rename), so it can be deleted at your leisure.
|
|
285
|
+
*/
|
|
286
|
+
renamedFrom(previous: string): ColumnBuilder<K, N, D, E, P, T>;
|
|
287
|
+
/**
|
|
288
|
+
* See {@link ColumnDef.ignored}.
|
|
289
|
+
*
|
|
290
|
+
* COPIES the def rather than mutating it. The constructor takes an existing
|
|
291
|
+
* def BY REFERENCE, so every builder derived from another shares one object —
|
|
292
|
+
* `const a = slug.unique()` leaves `a._def === slug._def`. An in-place
|
|
293
|
+
* `ignored = true` therefore marks every column sharing that def, including
|
|
294
|
+
* one another table actively reads, and the gate would let THAT column be
|
|
295
|
+
* dropped. Measured before this copy existed.
|
|
296
|
+
*
|
|
297
|
+
* The aliasing is older than this method and other fields leak through it too.
|
|
298
|
+
* The reason this one cannot wait: every other leak produces a VISIBLE schema
|
|
299
|
+
* difference — the plan shows it, the DDL shows it. This one is invisible by
|
|
300
|
+
* design (no DDL, no diff, no plan line), so its only effect is to disarm a
|
|
301
|
+
* safety gate in silence.
|
|
302
|
+
*/
|
|
303
|
+
ignored(): ColumnBuilder<K, N, D, E, P, T>;
|
|
304
|
+
/**
|
|
305
|
+
* Foreign key onto another table's column.
|
|
306
|
+
*
|
|
307
|
+
* The target is a THUNK, not a direct reference. In a cycle (`x → y`, `y → x`)
|
|
308
|
+
* the second table does not exist yet when the first is built; a direct
|
|
309
|
+
* reference makes TypeScript chase its own tail (TS7022 — measured, and making
|
|
310
|
+
* the return type independent of the target does NOT help). The thunk is
|
|
311
|
+
* invoked in `defineSchema`, where every binding exists and every table
|
|
312
|
+
* already knows its name.
|
|
313
|
+
*
|
|
314
|
+
* In a cycle, ONE side needs an explicit return type:
|
|
315
|
+
* `references((): AnyColumn => y.id)`. One side is enough — measured.
|
|
316
|
+
* For a self-reference use `selfReferences(column)`: no thunk, no annotation.
|
|
317
|
+
*
|
|
318
|
+
* `as` names the FORWARD relation (`author_id` → `author` by default);
|
|
319
|
+
* `reverseAs` names the REVERSE one on the parent, whose default is this
|
|
320
|
+
* table's own name (`users.posts`). Two foreign keys from one table onto one
|
|
321
|
+
* parent therefore need a `reverseAs` on at least one of them — the reverse
|
|
322
|
+
* names would otherwise both be this table's name.
|
|
323
|
+
*/
|
|
324
|
+
references(target: () => AnyColumn, opts?: {
|
|
325
|
+
as?: string;
|
|
326
|
+
reverseAs?: string;
|
|
327
|
+
onDelete?: OnDeleteAction;
|
|
328
|
+
/**
|
|
329
|
+
* Bu FK kolonu için TÜREVİ index üretilsin mi (FR-044). Varsayılan açık.
|
|
330
|
+
*
|
|
331
|
+
* Postgres bir foreign key'i otomatik indekslemez — yalnız hedef
|
|
332
|
+
* taraftaki unique kısıt vardır. Bedeli FK üzerinden her JOIN'de ve her
|
|
333
|
+
* `ON DELETE CASCADE`'de ödenir: bir parent silinirken child tablo tam
|
|
334
|
+
* taranır.
|
|
335
|
+
*
|
|
336
|
+
* `false` demek gerçek bir ihtiyaç, nezaket değil (D-028): FK kolonu aynı
|
|
337
|
+
* zamanda SIK GÜNCELLENEN bir kolonsa index HOT güncellemeyi kırar — ve
|
|
338
|
+
* HOT, DEĞİŞEN kolon indeksliyse kırılır, tablo indeksli diye değil.
|
|
339
|
+
*/
|
|
340
|
+
index?: boolean;
|
|
341
|
+
}): ColumnBuilder<K, N, D, E, P, T>;
|
|
342
|
+
/**
|
|
343
|
+
* Bu kolon `increment()` / `decrement()` ile güncelleniyor (FR-049).
|
|
344
|
+
*
|
|
345
|
+
* Deploy kontrolcü kodunu OKUMAZ, o yüzden bildirimin söylemesi gerekiyor —
|
|
346
|
+
* ve söylediği anda plan bir şeyi görebiliyor: aynı kolon hem sayaç hem
|
|
347
|
+
* indeksliyse her güncelleme HOT'u kaybeder.
|
|
348
|
+
*
|
|
349
|
+
* ZİNCİR (D-028+D-030): HOT kaybı → ölü tuple → autovacuum yükü → ve
|
|
350
|
+
* autovacuum worker'ları KÜME GENELİNDE bir kaynak, yani bedeli başka
|
|
351
|
+
* kiracıların tabloları da öder.
|
|
352
|
+
*
|
|
353
|
+
* Sektörde çare "dokümana uyarı yaz"dır, çünkü index'i ekleyen kişi
|
|
354
|
+
* `increment()`'i yazan kişi değildir. Bu bildirimde ikisi de YAN YANA
|
|
355
|
+
* duruyor.
|
|
356
|
+
*
|
|
357
|
+
* Uyarı, HATA DEĞİL: sayaç kolonunu indekslemek bazen doğru karardır.
|
|
358
|
+
*/
|
|
359
|
+
counter(): ColumnBuilder<K, N, D, E, P, T>;
|
|
360
|
+
/**
|
|
361
|
+
* Foreign key onto THIS table (`parent_id → id`) — category trees, comment
|
|
362
|
+
* replies, org charts.
|
|
363
|
+
*
|
|
364
|
+
* No thunk and no type annotation: the target table is the one being declared,
|
|
365
|
+
* so there is nothing to defer and nothing for TypeScript to chase in a circle.
|
|
366
|
+
* Drizzle forces an explicit `(): AnyPgColumn =>` here because its reference
|
|
367
|
+
* always goes through a callback; measured, we do not need one.
|
|
368
|
+
*/
|
|
369
|
+
selfReferences(column: string, opts?: {
|
|
370
|
+
as?: string;
|
|
371
|
+
onDelete?: OnDeleteAction;
|
|
372
|
+
}): ColumnBuilder<K, N, D, E, P, T>;
|
|
373
|
+
/** Set the ON DELETE action for a foreign key reference. */
|
|
374
|
+
onDelete(action: OnDeleteAction): ColumnBuilder<K, N, D, E, P, T>;
|
|
375
|
+
/** Add a single-column UNIQUE constraint. */
|
|
376
|
+
unique(): ColumnBuilder<K, N, D, E, P, T>;
|
|
377
|
+
/**
|
|
378
|
+
* Declare how this column's value is projected in and out of the process.
|
|
379
|
+
*
|
|
380
|
+
* The DDL does not move: `numeric` stays `numeric`, and the driver still hands
|
|
381
|
+
* back what Postgres sent. What changes is the type the row surface exposes —
|
|
382
|
+
* it becomes `Target`:
|
|
383
|
+
*
|
|
384
|
+
* amount: numeric().transform<number>({ fromDb: Number, toDb: String })
|
|
385
|
+
*
|
|
386
|
+
* `numeric` surfacing as `string` is CORRECT (a JS number cannot hold
|
|
387
|
+
* arbitrary precision), and that is exactly why this exists: application code
|
|
388
|
+
* that does arithmetic on the column otherwise rewrites the same
|
|
389
|
+
* `Number(row.amount)` / `String(x)` pair in every controller that touches it,
|
|
390
|
+
* and each rewrite is a place the two directions can drift apart.
|
|
391
|
+
*
|
|
392
|
+
* A transform is a PROJECTION, never a constraint: it lives only in this
|
|
393
|
+
* process, so it can neither validate nor migrate what is stored.
|
|
394
|
+
*/
|
|
395
|
+
transform<Target>(fns: ColumnTransform<Target>): ColumnBuilder<K, N, D, E, P, Target>;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Extracts the TypeScript value type for a column, respecting nullability.
|
|
399
|
+
* - "uuid" | "text" | "timestamp" | "bigint" | "numeric" → string (or string | null when N = true)
|
|
400
|
+
* Note: bigint/numeric surface as string — JS number loses precision past 2^53,
|
|
401
|
+
* and pgx/PostgREST serialize int8/numeric as strings. App code uses
|
|
402
|
+
* BigInt(row.amount) for bigint, or a decimal lib for numeric.
|
|
403
|
+
* - "integer" → number
|
|
404
|
+
* - "boolean" → boolean
|
|
405
|
+
* - "jsonb" → P (the dev-supplied payload shape from jsonb<T>(), else unknown)
|
|
406
|
+
* - "enum" → E (the union of literal values)
|
|
407
|
+
*
|
|
408
|
+
* A declared `.transform<T>()` OVERRIDES the table above: the column then
|
|
409
|
+
* surfaces as T (or T | null when nullable), because that is the value the
|
|
410
|
+
* application is handed. Nullability is still the column's, not the
|
|
411
|
+
* transform's — `fromDb` is not called for a NULL.
|
|
412
|
+
*/
|
|
413
|
+
type ColValue<C> = C extends ColumnBuilder<ColumnType, infer N, boolean, unknown, unknown, infer T> ? [unknown] extends [T] ? ColStoredValue<C> : N extends true ? T | null : T : never;
|
|
414
|
+
/** The value as the DATABASE hands it over — the branch table above, before any
|
|
415
|
+
* transform. This is what a column's `fromDb` receives. */
|
|
416
|
+
type ColStoredValue<C> = C extends ColumnBuilder<'uuid' | 'text' | 'timestamp' | 'bigint' | 'numeric', infer N, infer _D, infer _E, infer _P> ? N extends true ? string | null : string : C extends ColumnBuilder<'integer', infer N, infer _D, infer _E, infer _P> ? N extends true ? number | null : number : C extends ColumnBuilder<'boolean', infer N, infer _D, infer _E, infer _P> ? N extends true ? boolean | null : boolean : C extends ColumnBuilder<'jsonb', infer N, infer _D, infer _E, infer P> ? N extends true ? P | null : P : C extends ColumnBuilder<'vector', infer N, infer _D, infer _E, infer _P> ? N extends true ? number[] | null : number[] : C extends ColumnBuilder<'enum', infer N, infer _D, infer E, infer _P> ? N extends true ? E | null : E : never;
|
|
417
|
+
/**
|
|
418
|
+
* True when a column is optional on INSERT:
|
|
419
|
+
* - nullable columns (N = true) — the DB allows NULL so the field may be omitted
|
|
420
|
+
* - columns with a default (D = true) — the DB fills in the value when absent
|
|
421
|
+
*/
|
|
422
|
+
type ColIsOptionalOnInsert<C> = C extends ColumnBuilder<ColumnType, true, boolean, unknown, unknown, unknown> ? true : C extends ColumnBuilder<ColumnType, boolean, true, unknown, unknown, unknown> ? true : false;
|
|
423
|
+
/** Create a UUID column. */
|
|
424
|
+
declare function uuid(): ColumnBuilder<'uuid', false, false, never>;
|
|
425
|
+
/** Create a TEXT column. */
|
|
426
|
+
declare function text(): ColumnBuilder<'text', false, false, never>;
|
|
427
|
+
/** Create an INTEGER column. Emits int4 (max ~2.1B). */
|
|
428
|
+
declare function integer(): ColumnBuilder<'integer', false, false, never>;
|
|
429
|
+
/**
|
|
430
|
+
* Create a BIGINT column (Postgres int8, max ~9.2×10^18).
|
|
431
|
+
* Surfaces as `string` in row/insert types — JS number loses precision past 2^53
|
|
432
|
+
* and pgx/PostgREST serialize int8 as a JSON string. Use BigInt(row.column) in app code.
|
|
433
|
+
*/
|
|
434
|
+
declare function bigint(): ColumnBuilder<'bigint', false, false, never>;
|
|
435
|
+
/**
|
|
436
|
+
* Create a NUMERIC column (Postgres `numeric`/`decimal`, arbitrary precision).
|
|
437
|
+
* For exact fractional values (money with cents as a decimal, rates, weights)
|
|
438
|
+
* where int4/int8 don't fit. Surfaces as `string` in row/insert types — JS
|
|
439
|
+
* number can't hold arbitrary-precision decimals without rounding, and
|
|
440
|
+
* pgx/PostgREST serialize numeric as a JSON string. Parse with a decimal lib
|
|
441
|
+
* (or BigInt for scaled integers) in app code.
|
|
442
|
+
*/
|
|
443
|
+
declare function numeric(): ColumnBuilder<'numeric', false, false, never>;
|
|
444
|
+
/** Create a BOOLEAN column. */
|
|
445
|
+
declare function boolean(): ColumnBuilder<'boolean', false, false, never>;
|
|
446
|
+
/** Create a TIMESTAMP column. */
|
|
447
|
+
declare function timestamp(): ColumnBuilder<'timestamp', false, false, never>;
|
|
448
|
+
/**
|
|
449
|
+
* Create a JSONB column. Pass a payload type to make the generated row/insert
|
|
450
|
+
* type concrete instead of `unknown`:
|
|
451
|
+
*
|
|
452
|
+
* tags: jsonb<string[]>() // row.tags: string[]
|
|
453
|
+
* meta: jsonb<{ tier: string }>() // row.meta: { tier: string }
|
|
454
|
+
* raw: jsonb() // row.raw: unknown (back-compat)
|
|
455
|
+
*
|
|
456
|
+
* The runtime accepts a plain JS object/array directly (no JSON.stringify); the
|
|
457
|
+
* generic only refines the TYPE the env codegen emits.
|
|
458
|
+
*/
|
|
459
|
+
declare function jsonb<T = unknown>(): ColumnBuilder<'jsonb', false, false, never, T>;
|
|
460
|
+
/**
|
|
461
|
+
* Create an ENUM column.
|
|
462
|
+
* @param name The PostgreSQL enum type name (used in DDL).
|
|
463
|
+
* @param values A readonly tuple of valid string values — kept `const` so the
|
|
464
|
+
* union `V[number]` is as narrow as possible.
|
|
465
|
+
*/
|
|
466
|
+
declare function enumType<const V extends readonly string[]>(name: string, values: V): ColumnBuilder<'enum', false, false, V[number]>;
|
|
467
|
+
/** vector(n) — pgvector kolonu. n TİPİN parçasıdır (typmod) ve [1, 2000] —
|
|
468
|
+
* 2000 = pgvector'ün HNSW-indekslenebilir tavanı; auto-index bu beyanla bağlı
|
|
469
|
+
* (spec FR-001, D-3). */
|
|
470
|
+
declare function vector(dimensions: number): ColumnBuilder<'vector', false, false, unknown, number[]>;
|
|
471
|
+
/**
|
|
472
|
+
* Any column, whatever its type parameters.
|
|
473
|
+
*
|
|
474
|
+
* Exported so a cycle can be broken from ONE side:
|
|
475
|
+
* `references((): AnyColumn => y.id)`.
|
|
476
|
+
*/
|
|
477
|
+
type AnyColumn = ColumnBuilder;
|
|
478
|
+
/**
|
|
479
|
+
* The column that OWNS this row: a `text` FK onto `auth.users(id)`, NOT NULL,
|
|
480
|
+
* ON DELETE CASCADE.
|
|
481
|
+
*
|
|
482
|
+
* Ownership drives account erasure, so cascade is the only correct action and
|
|
483
|
+
* takes no argument. The referencing column must be `text` (palauth ids are
|
|
484
|
+
* `usr_<uuid>`) and NOT NULL — both are implied here rather than left to the
|
|
485
|
+
* caller, so three rules the type could not express before become UNWRITABLE.
|
|
486
|
+
*
|
|
487
|
+
* At most ONE per table. The old shape let several columns reference
|
|
488
|
+
* `auth.users` and picked the FIRST IN DECLARATION ORDER as the owner — moving a
|
|
489
|
+
* `created_by` above a `user_id` silently changed which rows an account deletion
|
|
490
|
+
* took with it. A second one is now rejected at push.
|
|
491
|
+
*
|
|
492
|
+
* For a column that merely POINTS at a user without owning the row
|
|
493
|
+
* (`created_by`, `edited_by`), use `userRef({ onDelete })`.
|
|
494
|
+
*/
|
|
495
|
+
declare function ownedByUser(): ColumnBuilder<'text', false, false, never>;
|
|
496
|
+
/**
|
|
497
|
+
* A plain FK onto `auth.users(id)` that does NOT own the row.
|
|
498
|
+
*
|
|
499
|
+
* `created_by` / `edited_by`: deleting that user must not delete the row. ON
|
|
500
|
+
* DELETE is required and limited to `cascade | set null` so an erasure request
|
|
501
|
+
* is never blocked by a lingering FK; `set null` needs a nullable column.
|
|
502
|
+
*/
|
|
503
|
+
declare function userRef(opts: {
|
|
504
|
+
onDelete: AuthUserOnDelete;
|
|
505
|
+
as?: string;
|
|
506
|
+
}): ColumnBuilder<'text', boolean, false, never>;
|
|
507
|
+
/**
|
|
508
|
+
* A plain FK onto `auth.installations(id)` — the app-scoped verified-device
|
|
509
|
+
* anchor.
|
|
510
|
+
*
|
|
511
|
+
* An installation is an APP INSTALL, not a user: this is NOT ownership. A
|
|
512
|
+
* user-owned row still needs its own `ownedByUser()` so account erasure removes
|
|
513
|
+
* it; an installation reference alone does not tie a row to a user's deletion.
|
|
514
|
+
*/
|
|
515
|
+
declare function installationRef(opts: {
|
|
516
|
+
onDelete: AuthUserOnDelete;
|
|
517
|
+
as?: string;
|
|
518
|
+
}): ColumnBuilder<'text', boolean, false, never>;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* policy.ts — the RLS policy authoring DSL.
|
|
522
|
+
*
|
|
523
|
+
* `policy(name)` returns a fluent builder that mirrors the `ColumnBuilder`
|
|
524
|
+
* style in columns.ts: each chainable method mutates the underlying
|
|
525
|
+
* definition and returns the builder so calls compose. The terminal value is
|
|
526
|
+
* a plain {@link PolicyDef} — the exact JSON shape the runtime's
|
|
527
|
+
* `schema_extract.js` reads off the bundled module and the Go side parses into
|
|
528
|
+
* `PolicyJSON` (CONTRACT-POLICY).
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* import { policy } from "@palbase/backend";
|
|
532
|
+
*
|
|
533
|
+
* policy("owner_select")
|
|
534
|
+
* .for("select")
|
|
535
|
+
* .to("authenticated")
|
|
536
|
+
* .using("owner = (select auth.uid())");
|
|
537
|
+
*/
|
|
538
|
+
/** Politika ifadesinin düğümleri. DÜZ VERİ: tele bu şekilde gider. */
|
|
539
|
+
type PolicyExpr = {
|
|
540
|
+
kind: "col";
|
|
541
|
+
name: string;
|
|
542
|
+
/**
|
|
543
|
+
* Hangi tablonun kolonu (FR-023). Yoksa POLİTİKANIN KENDİ tablosu —
|
|
544
|
+
* Postgres politika ifadesinde onu niteliksiz çözer. Doluysa bir
|
|
545
|
+
* `existsIn` alt-sorgusunun takma adı.
|
|
546
|
+
*
|
|
547
|
+
* Bu ayrım korelasyonun tamamı: ayırt edilmezse `team_id = team_id`
|
|
548
|
+
* üretilir, her satır için doğrudur, ve politika HİÇBİR ŞEYİ süzmez —
|
|
549
|
+
* yani tüm belgeler herkese görünür.
|
|
550
|
+
*/
|
|
551
|
+
alias?: string;
|
|
552
|
+
} | {
|
|
553
|
+
kind: "authUid";
|
|
554
|
+
} | {
|
|
555
|
+
kind: "lit";
|
|
556
|
+
value: string | number | boolean | null;
|
|
557
|
+
} | {
|
|
558
|
+
kind: "binop";
|
|
559
|
+
op: PolicyBinOp;
|
|
560
|
+
left: PolicyExpr;
|
|
561
|
+
right: PolicyExpr;
|
|
562
|
+
} | {
|
|
563
|
+
kind: "and";
|
|
564
|
+
parts: PolicyExpr[];
|
|
565
|
+
} | {
|
|
566
|
+
kind: "or";
|
|
567
|
+
parts: PolicyExpr[];
|
|
568
|
+
} | {
|
|
569
|
+
kind: "not";
|
|
570
|
+
expr: PolicyExpr;
|
|
571
|
+
} | {
|
|
572
|
+
kind: "isNull";
|
|
573
|
+
expr: PolicyExpr;
|
|
574
|
+
negated: boolean;
|
|
575
|
+
} | {
|
|
576
|
+
/**
|
|
577
|
+
* Korelasyonlu `EXISTS (SELECT 1 FROM <table> <alias> WHERE <predicate>)`.
|
|
578
|
+
*
|
|
579
|
+
* FR-024 BAĞLAYICI: alt-sorgu DOĞRUDAN üretilir, bir `SECURITY DEFINER`
|
|
580
|
+
* fonksiyona SARILMAZ. Ölçüldü (spec Research Appendix, pg16): doğrudan
|
|
581
|
+
* alt-sorguda hedef tablonun KENDİ RLS'i uygulanıyor — yani sızıntı
|
|
582
|
+
* kanalı yok. Sarmalama ise onu bypass ederdi.
|
|
583
|
+
*/
|
|
584
|
+
kind: "exists";
|
|
585
|
+
table: string;
|
|
586
|
+
alias: string;
|
|
587
|
+
/**
|
|
588
|
+
* POLİTİKANIN KENDİ tablosunun adı — korelasyonun çalışması için ŞART.
|
|
589
|
+
*
|
|
590
|
+
* ÖLÇÜLDÜ (canlı pg16): alt-sorgu içinde niteliksiz bir kolon adı
|
|
591
|
+
* Postgres tarafından İÇ tabloya çözülüyor. Yani dış kolon niteliksiz
|
|
592
|
+
* bırakılınca `pb_e1.team_id = pb_e1.team_id` üretiliyor — her satır
|
|
593
|
+
* için doğru, ve politika HİÇBİR ŞEYİ süzmüyor: iki farklı kullanıcı
|
|
594
|
+
* da tüm belgeleri gördü. Dış tablo adı burada taşınıyor ki emitter
|
|
595
|
+
* `"documents"."team_id"` yazabilsin.
|
|
596
|
+
*/
|
|
597
|
+
outer: string;
|
|
598
|
+
predicate: PolicyExpr;
|
|
599
|
+
};
|
|
600
|
+
/** Karşılaştırma operatörleri. `in` sağ tarafta liste bekler. */
|
|
601
|
+
type PolicyBinOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in";
|
|
602
|
+
/**
|
|
603
|
+
* Bir ifadenin ETRAFINDAKİ akıcı sarmalayıcı.
|
|
604
|
+
*
|
|
605
|
+
* Düğüm (`expr`) düz veri kalıyor, metotlar sarmalayıcıda yaşıyor: `_def.using`
|
|
606
|
+
* serileştiğinde metotlu bir nesne değil, tam olarak tele gidecek şekil olsun
|
|
607
|
+
* diye. `.using()` sarmalayıcıyı açar.
|
|
608
|
+
*/
|
|
609
|
+
declare class PolicyExprRef {
|
|
610
|
+
readonly expr: PolicyExpr;
|
|
611
|
+
constructor(expr: PolicyExpr);
|
|
612
|
+
private bin;
|
|
613
|
+
eq(other: PolicyOperand): PolicyExprRef;
|
|
614
|
+
neq(other: PolicyOperand): PolicyExprRef;
|
|
615
|
+
gt(other: PolicyOperand): PolicyExprRef;
|
|
616
|
+
gte(other: PolicyOperand): PolicyExprRef;
|
|
617
|
+
lt(other: PolicyOperand): PolicyExprRef;
|
|
618
|
+
lte(other: PolicyOperand): PolicyExprRef;
|
|
619
|
+
/**
|
|
620
|
+
* `IS NULL` / `IS NOT NULL` — `= NULL` SQL'de her zaman UNKNOWN'dır, o yüzden
|
|
621
|
+
* niyet ayrı bir düğümle yazılır. Partial index'in en sık koşulu
|
|
622
|
+
* (`WHERE deleted_at IS NULL`) tam olarak bu.
|
|
623
|
+
*/
|
|
624
|
+
isNull(negated?: boolean): PolicyExprRef;
|
|
625
|
+
and(...others: PolicyExprRef[]): PolicyExprRef;
|
|
626
|
+
or(...others: PolicyExprRef[]): PolicyExprRef;
|
|
627
|
+
not(): PolicyExprRef;
|
|
628
|
+
}
|
|
629
|
+
/** `.eq()` gibi metotların sağ tarafı: başka bir ifade ya da düz bir değer. */
|
|
630
|
+
type PolicyOperand = PolicyExprRef | string | number | boolean | null;
|
|
631
|
+
/**
|
|
632
|
+
* Politika ifadesi bağlamı — `defineTable`'ın `policies` callback'ine verilir.
|
|
633
|
+
*
|
|
634
|
+
* Kolon adları TABLONUN KENDİ kolonlarıyla sınırlıdır (FR-022). Bu, `policies`
|
|
635
|
+
* bir dizi değil de callback olduğu için mümkün: TypeScript `columns`'tan
|
|
636
|
+
* `C`'yi çıkarıp callback'i onunla bağlamlandırıyor. `policy("x")` tek başına
|
|
637
|
+
* çağrıldığında hangi tabloda olduğunu bilemezdi.
|
|
638
|
+
*/
|
|
639
|
+
interface PolicyExprCtx<Cols extends string = string> {
|
|
640
|
+
/** Bu tablonun bir kolonu. Bilinmeyen ad DERLENMEZ. */
|
|
641
|
+
col(name: Cols): PolicyExprRef;
|
|
642
|
+
/**
|
|
643
|
+
* "Bu satırın sahibi olduğum bir takıma ait mi?" — üyelik tabanlı kiracılık
|
|
644
|
+
* (FR-023), korelasyonlu bir `EXISTS` alt-sorgusu olarak.
|
|
645
|
+
*
|
|
646
|
+
* ```ts
|
|
647
|
+
* policies: (p) => [
|
|
648
|
+
* policy("team_read").for("select").using(
|
|
649
|
+
* p.existsIn(teamMembers, (m) =>
|
|
650
|
+
* m.col("team_id").eq(p.col("team_id"))
|
|
651
|
+
* .and(m.col("user_id").eq(p.auth.uid()))),
|
|
652
|
+
* ),
|
|
653
|
+
* ]
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
656
|
+
* `m` ÜYELİK tablosunun kolonlarını, `p` bu tablonunkileri verir; ikisi de
|
|
657
|
+
* tipli ve SQL'de farklı nitelenirler. Karışsalardı `team_id = team_id`
|
|
658
|
+
* üretilir ve politika hiçbir şeyi süzmezdi.
|
|
659
|
+
*
|
|
660
|
+
* ÖZYİNELEME TUZAĞI: üyelik tablosunun KENDİ politikası `existsIn`
|
|
661
|
+
* KULLANMAZ — `user_id = auth.uid()` yazar. İki tablo birbirini
|
|
662
|
+
* sorgularsa Postgres sorgu anında
|
|
663
|
+
* `infinite recursion detected in policy for relation …` der. Asimetri
|
|
664
|
+
* kasıtlıdır: üyelik tablosu basit olanı alır, diğer her tablo ona
|
|
665
|
+
* alt-sorgu ATAR.
|
|
666
|
+
*/
|
|
667
|
+
existsIn<C extends ColumnMap, N extends string>(table: TableHandle<C, N>, predicate: (row: PolicyExprCtx<Extract<keyof C, string>>) => PolicyExprRef): PolicyExprRef;
|
|
668
|
+
/** Çağıranın kimliği. SQL'e `(select auth.uid())` olarak iner (InitPlan). */
|
|
669
|
+
auth: {
|
|
670
|
+
uid(): PolicyExprRef;
|
|
671
|
+
};
|
|
672
|
+
/** Düz bir değer — karşılaştırmanın sağ tarafında açıkça literal demek için. */
|
|
673
|
+
lit(v: string | number | boolean | null): PolicyExprRef;
|
|
674
|
+
and(...parts: PolicyExprRef[]): PolicyExprRef;
|
|
675
|
+
or(...parts: PolicyExprRef[]): PolicyExprRef;
|
|
676
|
+
not(part: PolicyExprRef): PolicyExprRef;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* İfade bağlamını kurar. `defineTable` bunu kendi kolon adlarıyla çağırır;
|
|
680
|
+
* testler ve elle kullanım için de dışa açık.
|
|
681
|
+
*/
|
|
682
|
+
declare function exprCtx<Cols extends string = string>(_columns?: readonly Cols[], depth?: number,
|
|
683
|
+
/**
|
|
684
|
+
* POLİTİKANIN KENDİ tablosu — her derinlikte AYNI kalır.
|
|
685
|
+
*
|
|
686
|
+
* Alt-sorgunun içinde takma adsız duran bir kolon, dış sorgudaki tabloya
|
|
687
|
+
* bağlanmak zorundadır; Go tarafı onu bu adla nitelendiriyor. Değer iç içe
|
|
688
|
+
* geçmede ORTA tablonun adına kayıyordu (`meta.name` aşağı veriliyordu) ve
|
|
689
|
+
* sonuç canlıda `ERROR: missing FROM-clause entry for table "…"` oluyordu —
|
|
690
|
+
* yani iç içe `existsIn` içinden politikanın kendi kolonuna erişmek imkânsızdı
|
|
691
|
+
* (nihai inceleme I-1, ölçüldü). Kök tablo aşağı DEĞİŞMEDEN taşınır.
|
|
692
|
+
*/
|
|
693
|
+
ownTable?: string): PolicyExprCtx<Cols>;
|
|
694
|
+
/** The SQL command a policy applies to. `"all"` covers SELECT/INSERT/UPDATE/DELETE. */
|
|
695
|
+
type PolicyCommand = "all" | "select" | "insert" | "update" | "delete";
|
|
696
|
+
/** Whether a policy is permissive (OR-combined, the default) or restrictive
|
|
697
|
+
* (AND-combined). Mirrors Postgres `CREATE POLICY ... AS PERMISSIVE|RESTRICTIVE`. */
|
|
698
|
+
type PolicyMode = "permissive" | "restrictive";
|
|
699
|
+
/**
|
|
700
|
+
* The compiled, serializable policy definition — the EXACT shape consumed by
|
|
701
|
+
* `schema_extract.js` → Go `PolicyJSON` (CONTRACT-POLICY).
|
|
702
|
+
*
|
|
703
|
+
* - `roles`: the DB roles this policy applies to (`TO` clause). An empty array
|
|
704
|
+
* means the policy applies to PUBLIC (all roles) — the Postgres default.
|
|
705
|
+
* - `using`: the `USING (...)` row-visibility expression, or `null` when none.
|
|
706
|
+
* - `withCheck`: the `WITH CHECK (...)` write-validation expression, or `null`.
|
|
707
|
+
* - `permissive`: `true` for `AS PERMISSIVE` (default), `false` for restrictive.
|
|
708
|
+
*/
|
|
709
|
+
interface PolicyDef {
|
|
710
|
+
name: string;
|
|
711
|
+
command: PolicyCommand;
|
|
712
|
+
roles: string[];
|
|
713
|
+
/** Yapı (FR-022) ya da ham string (FR-028 kaçış kapağı). */
|
|
714
|
+
using: string | PolicyExpr | null;
|
|
715
|
+
withCheck: string | PolicyExpr | null;
|
|
716
|
+
permissive: boolean;
|
|
717
|
+
}
|
|
718
|
+
declare class PolicyBuilder {
|
|
719
|
+
readonly _def: PolicyDef;
|
|
720
|
+
constructor(name: string);
|
|
721
|
+
/** Restrict the policy to a single SQL command (default `"all"`). */
|
|
722
|
+
for(command: PolicyCommand): this;
|
|
723
|
+
/**
|
|
724
|
+
* Set the DB roles the policy applies to (the `TO` clause), replacing any
|
|
725
|
+
* previously-set roles. Call with no arguments to target PUBLIC (all roles).
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* policy("p").to("authenticated")
|
|
729
|
+
* policy("p").to("authenticated", "service_role")
|
|
730
|
+
* policy("p").to() // PUBLIC
|
|
731
|
+
*/
|
|
732
|
+
to(...roles: string[]): this;
|
|
733
|
+
/**
|
|
734
|
+
* `USING (...)` satır görünürlüğü ifadesi.
|
|
735
|
+
*
|
|
736
|
+
* Tercih edilen biçim YAPI'dır (FR-022) — `policies` callback'inin verdiği
|
|
737
|
+
* bağlamla kurulur ve kolon adları derlemede doğrulanır:
|
|
738
|
+
*
|
|
739
|
+
* ```ts
|
|
740
|
+
* policies: (p) => [
|
|
741
|
+
* policy("owner_read").for("select").using(p.col("owner").eq(p.auth.uid())),
|
|
742
|
+
* ]
|
|
743
|
+
* ```
|
|
744
|
+
*
|
|
745
|
+
* Ham string KAÇIŞ KAPAĞI olarak kalır (FR-028): yapının ifade edemediği bir
|
|
746
|
+
* şey gerektiğinde yol kapanmaz. Ama o yolda kolon adı doğrulaması yoktur ve
|
|
747
|
+
* hata deploy anında Postgres'ten gelir.
|
|
748
|
+
*/
|
|
749
|
+
using(expr: string | PolicyExprRef): this;
|
|
750
|
+
/**
|
|
751
|
+
* "Rows of THIS table whose owner the caller is a member of" — the membership
|
|
752
|
+
* pattern, written so it cannot recurse.
|
|
753
|
+
*
|
|
754
|
+
* THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
|
|
755
|
+
* other: `channels` is visible to members, so its policy reads
|
|
756
|
+
* `channel_members`; `channel_members` is visible to members, so its policy
|
|
757
|
+
* reads `channels`. Postgres refuses the pair at query time with `infinite
|
|
758
|
+
* recursion detected in policy for relation ...`, and the error names the
|
|
759
|
+
* relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
|
|
760
|
+
* is protected by `user_id = auth.uid()` and nothing else, and every other
|
|
761
|
+
* table subqueries INTO it. That shape was in the platform's own schema and
|
|
762
|
+
* written down nowhere; a customer recovered it by reading that schema.
|
|
763
|
+
*
|
|
764
|
+
* `(select auth.uid())` rather than a bare call: the scalar subquery is
|
|
765
|
+
* evaluated ONCE per statement instead of per row.
|
|
766
|
+
*
|
|
767
|
+
* @example
|
|
768
|
+
* // channels: visible to members. The membership table gets the simple one.
|
|
769
|
+
* policy("member_read").for("select").to("authenticated")
|
|
770
|
+
* .memberOf("channel_members", "channel_id")
|
|
771
|
+
* // → id IN (SELECT "channel_id" FROM "channel_members"
|
|
772
|
+
* // WHERE "user_id" = (select auth.uid()))
|
|
773
|
+
*/
|
|
774
|
+
memberOf(membershipTable: string, foreignKey: string, options?: {
|
|
775
|
+
column?: string;
|
|
776
|
+
userColumn?: string;
|
|
777
|
+
}): this;
|
|
778
|
+
/** `WITH CHECK (...)` yazma doğrulaması — `using` ile aynı iki biçim. */
|
|
779
|
+
withCheck(expr: string | PolicyExprRef): this;
|
|
780
|
+
/** Set the policy mode: `"permissive"` (default, OR-combined) or
|
|
781
|
+
* `"restrictive"` (AND-combined). */
|
|
782
|
+
as(mode: PolicyMode): this;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Start authoring an RLS policy. Returns a {@link PolicyBuilder}; the resulting
|
|
786
|
+
* `PolicyBuilder` is accepted directly in a table's `policies: [...]` array
|
|
787
|
+
* (its `_def` is read at schema-extract time).
|
|
788
|
+
*
|
|
789
|
+
* @param name The policy name. Palbase reconciliation keys policies by
|
|
790
|
+
* `(table, name)`, so names must be unique per table.
|
|
791
|
+
*/
|
|
792
|
+
declare function policy(name: string): PolicyBuilder;
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Postgres extensions a Palbase project can enable from its schema.
|
|
796
|
+
*
|
|
797
|
+
* Extensions are config-as-code: declare them in `defineSchema({ extensions })`
|
|
798
|
+
* and the deploy installs them (CREATE EXTENSION … SCHEMA extensions) using the
|
|
799
|
+
* deploy path's privileged connection. They are NOT toggled live from Studio —
|
|
800
|
+
* CREATE EXTENSION requires a superuser role that only the deploy path holds.
|
|
801
|
+
*
|
|
802
|
+
* The list is an allowlist (a string-literal union) so editors autocomplete the
|
|
803
|
+
* supported names and a typo fails typecheck. It is intentionally extensible:
|
|
804
|
+
* add a name here (+ confirm the base image ships it) to support more.
|
|
805
|
+
*/
|
|
806
|
+
declare const PALBASE_EXTENSIONS: readonly ["vector", "pg_trgm", "unaccent", "citext", "cube", "earthdistance", "hstore", "ltree", "btree_gist", "pgcrypto", "uuid-ossp"];
|
|
807
|
+
/** A Postgres extension supported by Palbase (allowlist union). */
|
|
808
|
+
type PalbaseExtension = (typeof PALBASE_EXTENSIONS)[number];
|
|
809
|
+
/**
|
|
810
|
+
* Extensions that depend on another extension. The deploy installs
|
|
811
|
+
* dependencies first; declaring `earthdistance` without `cube` still works
|
|
812
|
+
* because the deploy resolves the order, but listing both is clearer.
|
|
813
|
+
*/
|
|
814
|
+
declare const EXTENSION_DEPENDENCIES: Partial<Record<PalbaseExtension, PalbaseExtension[]>>;
|
|
815
|
+
/** Runtime guard: is `name` a supported Palbase extension? */
|
|
816
|
+
declare function isPalbaseExtension(name: string): name is PalbaseExtension;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* A named raw-SQL DDL object declared in a schema file (`db/public.ts`,
|
|
820
|
+
* `db/billing.ts`, …) for anything the typed DSL
|
|
821
|
+
* cannot express (EXCLUDE, CHECK, partial/expression indexes, triggers, views).
|
|
822
|
+
* The deploy emits `up` verbatim on the privileged DDL connection — same trust
|
|
823
|
+
* posture as policy().using(). Tracked by NAME (not by diffing the body), so a
|
|
824
|
+
* changed body needs a new name or an explicit drop+add.
|
|
825
|
+
*/
|
|
826
|
+
interface RawConstraintDef {
|
|
827
|
+
name: string;
|
|
828
|
+
up: string;
|
|
829
|
+
down?: string;
|
|
830
|
+
}
|
|
831
|
+
declare function raw(name: string, up: string, opts?: {
|
|
832
|
+
down?: string;
|
|
833
|
+
}): RawConstraintDef;
|
|
834
|
+
|
|
835
|
+
/** Embedding sağlayıcı DESKRIPTORU — canlı istemci değil, düz veri: şemayla
|
|
836
|
+
* birlikte serileşir, çağrıyı Go worker (yazma) ve engine (sorgu) yapar.
|
|
837
|
+
* Adlandırma Vercel AI SDK'nın aynasıdır (openai.embedding("...")) ama paket
|
|
838
|
+
* bağımlılığı bilinçli olarak YOKTUR (spec C-3, UD-016). v1 sağlayıcı: openai (D-10). */
|
|
839
|
+
interface EmbeddingModelRef {
|
|
840
|
+
provider: "openai";
|
|
841
|
+
model: string;
|
|
842
|
+
dimensions?: number;
|
|
843
|
+
apiKeyName?: string;
|
|
844
|
+
baseURL?: string;
|
|
845
|
+
}
|
|
846
|
+
/** Chat/damıtma modeli DESKRIPTORU (C-11, D-019) — memory beyanının extract'i.
|
|
847
|
+
* Embedding gibi düz veridir; çağrıyı worker yapar, anahtar vault'taki
|
|
848
|
+
* OPENAI_API_KEY'dir (D-017: aynı sağlayıcı, yeni dış sistem yok). */
|
|
849
|
+
interface ChatModelRef {
|
|
850
|
+
provider: "openai";
|
|
851
|
+
model: string;
|
|
852
|
+
}
|
|
853
|
+
declare const openai: {
|
|
854
|
+
embedding(model: string, opts?: {
|
|
855
|
+
dimensions?: number;
|
|
856
|
+
apiKeyName?: string;
|
|
857
|
+
baseURL?: string;
|
|
858
|
+
}): EmbeddingModelRef;
|
|
859
|
+
chat(model: string): ChatModelRef;
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* A map of column builders keyed by column name — the value you write under
|
|
864
|
+
* the `columns` key of `defineTable("<name>", { columns })`.
|
|
865
|
+
*
|
|
866
|
+
* The default `Record<string, ColumnBuilder>` keeps bare references compiling
|
|
867
|
+
* without a type argument.
|
|
868
|
+
*/
|
|
869
|
+
type ColumnMap = Record<string, ColumnBuilder>;
|
|
870
|
+
/**
|
|
871
|
+
* The author-facing value written under each table key:
|
|
872
|
+
* `{ columns, rls?, policies? }`.
|
|
873
|
+
*
|
|
874
|
+
* - `columns`: the column map (required).
|
|
875
|
+
* - `rls`: enable + FORCE row-level security on this table. **Defaults to
|
|
876
|
+
* `true`**, and is forced on when `policies` is non-empty. A table with RLS
|
|
877
|
+
* and no policies is deny-all, which is the starting state: nothing reads it
|
|
878
|
+
* until a policy says who may. Set `rls: false` only for a genuinely public
|
|
879
|
+
* table — it is an explicit opt-out that a reviewer can grep for, not
|
|
880
|
+
* something you get by forgetting.
|
|
881
|
+
* - `policies`: the RLS policies for this table, authored with `policy(name)`.
|
|
882
|
+
* Each entry may be a {@link PolicyBuilder} (the normal `policy(...)` chain)
|
|
883
|
+
* or a raw {@link PolicyDef} object.
|
|
884
|
+
*
|
|
885
|
+
* The `C` type parameter preserves the precise per-column phantom types so the
|
|
886
|
+
* typed `Database.public.*` surface keeps inferring insert/row shapes.
|
|
887
|
+
*/
|
|
888
|
+
/** Arama metriği — tek kelime; opclass ve operatör bundan türetilir, asla yüzeye çıkmaz (D-5/D-6). */
|
|
889
|
+
type SearchMetric = "cosine" | "euclidean" | "inner_product";
|
|
890
|
+
/** Bir vektör arama kolu. `model` varsa auto-embed: platform yazma+sorgu embedding'ini üstlenir (D-9). */
|
|
891
|
+
interface VectorSearchDecl {
|
|
892
|
+
/** Hedef vector kolonu; tabloda TEK vector kolonu varsa atlanabilir (Go apply çözer, FR-010). */
|
|
893
|
+
column?: string;
|
|
894
|
+
/** Varsa auto-embed. Deskriptor düz veridir — şemayla serileşir (C-3). */
|
|
895
|
+
model?: EmbeddingModelRef;
|
|
896
|
+
/** model varsa ZORUNLU: embed kaynak kolonları = trigger'ın UPDATE OF listesi (C-2). */
|
|
897
|
+
from?: string[];
|
|
898
|
+
metric?: SearchMetric;
|
|
899
|
+
/** Kaynak metin değişince ara-dönem davranışı (yalnız auto-embed'de):
|
|
900
|
+
* "null" (vars.) — embedding anında NULL'lanır; satır yeni vektör yazılana
|
|
901
|
+
* dek anlamsal aramada aday değildir. Bayat eşleşme asla servis edilmez.
|
|
902
|
+
* "keep" — eski vektör aramada kalır, platform yenisini yazınca sessizce
|
|
903
|
+
* değişir. Görünürlük penceresi sıfır; bedeli saniyeler süren bayat
|
|
904
|
+
* eşleşme riski (Confluence-tipi sync yükleri için). */
|
|
905
|
+
staleness?: "null" | "keep";
|
|
906
|
+
}
|
|
907
|
+
/** Tablonun arama beyanı — İKİ biçim (D-007, tek yüzey):
|
|
908
|
+
*
|
|
909
|
+
* YENİ (önerilen): `{ from, model, ... }` — `from` kolonları hem FTS'e hem
|
|
910
|
+
* embed'e girer. Tabloda vector kolonu declare edilmişse SATIR-modu; yoksa
|
|
911
|
+
* CHUNK-modu otomatiktir (D-010): vektörler türev `__palbase_chunks`
|
|
912
|
+
* tablosunda yaşar, içerik otomatik bölünür. `text: false` FTS'i kapatır,
|
|
913
|
+
* `text: [..]` FTS kolonlarını from'dan ayırır. `chunks` yalnız ince ayar.
|
|
914
|
+
*
|
|
915
|
+
* ESKİ: `text: string[]` + `vector: {...}` — aynen çalışır, wire çıktısı
|
|
916
|
+
* bayt-aynı kalır (NFR-B1). İki biçim KARIŞTIRILAMAZ. */
|
|
917
|
+
interface SearchDecl {
|
|
918
|
+
text?: string[] | boolean;
|
|
919
|
+
vector?: VectorSearchDecl | VectorSearchDecl[];
|
|
920
|
+
/** Yeni biçim: arama kaynağı kolonlar (FTS + embed). Varlığı yeni biçimi seçer. */
|
|
921
|
+
from?: string[];
|
|
922
|
+
/** Yeni biçim: auto-embed modeli (zorunlu — BYO için eski biçimi kullanın). */
|
|
923
|
+
model?: EmbeddingModelRef;
|
|
924
|
+
metric?: SearchMetric;
|
|
925
|
+
staleness?: "null" | "keep";
|
|
926
|
+
/** Chunk-modu ince ayarı (yalnız vector kolonsuz tabloda anlamlı). */
|
|
927
|
+
chunks?: {
|
|
928
|
+
size?: number;
|
|
929
|
+
overlap?: number;
|
|
930
|
+
};
|
|
931
|
+
/** Sorgu-yeniden-yazımı: tek yönlü eş anlamlı haritası (FR-026). */
|
|
932
|
+
synonyms?: Record<string, string[]>;
|
|
933
|
+
/** Geçerlilik kolonları türetilir; arama varsayılan yalnız günceli tarar (FR-029). */
|
|
934
|
+
validity?: boolean;
|
|
935
|
+
}
|
|
936
|
+
/** Hafıza beyanı (FR-032, D-019): kaynak tablonun yazımlarından platform
|
|
937
|
+
* fact damıtır ve `into` tablosuna yazar. Hedef NORMAL declared tablodur —
|
|
938
|
+
* kendi search/unique/validity beyanlarıyla. Okuma = Database.$search(into).
|
|
939
|
+
* subject default "owner": fact'in kime ait olduğu kolonu (iki tabloda da). */
|
|
940
|
+
interface MemoryDecl {
|
|
941
|
+
from: string[];
|
|
942
|
+
into: string;
|
|
943
|
+
extract: ChatModelRef;
|
|
944
|
+
subject?: string;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Bir index bildiriminin girdi şekli (FR-041…043).
|
|
948
|
+
*
|
|
949
|
+
* `columns` ile `expression` birbirinin alternatifidir: biri kolon listesi,
|
|
950
|
+
* diğeri `lower(email)` gibi bir ifade. `where` partial index koşuludur ve
|
|
951
|
+
* filtre şekli sorgu tarafındaki `WhereFilter` ile aynıdır — iki ayrı filtre
|
|
952
|
+
* dili olmaz.
|
|
953
|
+
*/
|
|
954
|
+
/**
|
|
955
|
+
* Bir index'in TEL şekli — `IndexJSON` ile aynı alanlar (T002).
|
|
956
|
+
*
|
|
957
|
+
* Bildirilmeyen alan HİÇ yazılmaz: sıfır değer yazmak, bildirmeyen her index'i
|
|
958
|
+
* diff'te değişmiş gösterir ve her deploy'a boş bir migration ekler.
|
|
959
|
+
*/
|
|
960
|
+
interface IndexDef {
|
|
961
|
+
name: string;
|
|
962
|
+
columns?: string[];
|
|
963
|
+
expression?: string;
|
|
964
|
+
where?: PolicyExpr;
|
|
965
|
+
sort?: "asc" | "desc";
|
|
966
|
+
nulls?: "first" | "last";
|
|
967
|
+
include?: string[];
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* `index("<ad>")` — C-5 (FR-041…043).
|
|
971
|
+
*
|
|
972
|
+
* ```ts
|
|
973
|
+
* indexes: (c) => [
|
|
974
|
+
* index("orders_open")
|
|
975
|
+
* .on(c.col("status")) // kolon TİPLİ
|
|
976
|
+
* .where(c.col("deleted_at").isNull()) // partial
|
|
977
|
+
* .include(["total"]), // covering
|
|
978
|
+
* ]
|
|
979
|
+
* ```
|
|
980
|
+
*
|
|
981
|
+
* `policy()` ile AYNI dili konuşur: `where` politika ifade yapısının ta kendisi.
|
|
982
|
+
* Üçüncü bir filtre lehçesi açmak, aynı sorunun iki yazımı demekti.
|
|
983
|
+
*
|
|
984
|
+
* Kolon bağlamı callback'ten geliyor çünkü index tablo tanımının İÇİNDE
|
|
985
|
+
* bildiriliyor — `orders.status` yazmak, `orders` bağlaması o noktada henüz
|
|
986
|
+
* kurulmadığı için mümkün değil (T015'te `policies` için ölçülen aynı kısıt).
|
|
987
|
+
*/
|
|
988
|
+
declare class IndexBuilder {
|
|
989
|
+
readonly _def: IndexDef;
|
|
990
|
+
constructor(name: string);
|
|
991
|
+
/** Index'in kolonları. SIRA KORUNUR — çok kolonlu bir index'in anlamı sırasıdır. */
|
|
992
|
+
on(...cols: PolicyExprRef[]): this;
|
|
993
|
+
/**
|
|
994
|
+
* İfade index'i (`lower(email)`), kolon yerine.
|
|
995
|
+
*
|
|
996
|
+
* Metin VERBATIM emit ediliyor — `policy().using("…")` ile aynı güven duruşu.
|
|
997
|
+
* Tipli yapı bunu ifade edemiyor ve kaçış kapağını kapatmak, yazarı elle
|
|
998
|
+
* migration yazmaya iterdi.
|
|
999
|
+
*/
|
|
1000
|
+
onExpression(expr: string): this;
|
|
1001
|
+
/** Partial index koşulu — politika ifade dilinin ta kendisi. */
|
|
1002
|
+
where(expr: PolicyExprRef): this;
|
|
1003
|
+
/** Sıra. Tek yönlü index'te sıra genelde önemsizdir; ORDER BY ile eşleşen
|
|
1004
|
+
* bir index arandığında değildir. */
|
|
1005
|
+
desc(): this;
|
|
1006
|
+
asc(): this;
|
|
1007
|
+
/** NULL'ların yeri — `ORDER BY … NULLS LAST` ile eşleşmesi için. */
|
|
1008
|
+
nulls(where: "first" | "last"): this;
|
|
1009
|
+
/** Covering index (`INCLUDE`): index-only scan için taşınan ama
|
|
1010
|
+
* ARANMAYAN kolonlar. */
|
|
1011
|
+
include(cols: string[]): this;
|
|
1012
|
+
}
|
|
1013
|
+
declare function index(name: string): IndexBuilder;
|
|
1014
|
+
interface TableInput<C extends ColumnMap = ColumnMap> {
|
|
1015
|
+
columns: C;
|
|
1016
|
+
rls?: boolean;
|
|
1017
|
+
/**
|
|
1018
|
+
* Bu tablonun RLS politikaları — TABLONUN KENDİ KOLONLARININ bir fonksiyonu
|
|
1019
|
+
* olarak (FR-022).
|
|
1020
|
+
*
|
|
1021
|
+
* Callback olmasının sebebi tip: `columns`'tan çıkarılan `C`, TypeScript
|
|
1022
|
+
* tarafından buraya bağlamlandırılıyor, yani `p.col("ownerr")` yazım hatası
|
|
1023
|
+
* DERLEME hatası oluyor. Düz bir dizide `policy("x")` tek başına çağrıldığı
|
|
1024
|
+
* için hangi tabloda olduğunu bilemez, ve yanlış kolon adı ancak deploy
|
|
1025
|
+
* anında `CREATE POLICY` çalışırken Postgres'ten dönerdi. Bir RLS
|
|
1026
|
+
* politikasında yanlış kolon adı yetki hatasıdır; derlemede yakalanır.
|
|
1027
|
+
*
|
|
1028
|
+
* ```ts
|
|
1029
|
+
* policies: (p) => [
|
|
1030
|
+
* policy("owner_read").for("select").using(p.col("owner").eq(p.auth.uid())),
|
|
1031
|
+
* ]
|
|
1032
|
+
* ```
|
|
1033
|
+
*/
|
|
1034
|
+
policies?: (p: PolicyExprCtx<Extract<keyof C, string>>) => (PolicyBuilder | PolicyDef)[];
|
|
1035
|
+
/** Composite/named primary key (ordered column names). Omit for single-column inline .primaryKey(). */
|
|
1036
|
+
primaryKey?: string[];
|
|
1037
|
+
/** Named multi-column UNIQUE constraints. */
|
|
1038
|
+
unique?: {
|
|
1039
|
+
name: string;
|
|
1040
|
+
columns: string[];
|
|
1041
|
+
}[];
|
|
1042
|
+
/** Named raw-SQL DDL objects (EXCLUDE, triggers, views) that the typed DSL cannot express. */
|
|
1043
|
+
raw?: RawConstraintDef[];
|
|
1044
|
+
/**
|
|
1045
|
+
* Named first-class CHECK constraints. Diffed by NAME with a BODY compare:
|
|
1046
|
+
* a changed `expr` (after pg normalization) recreates the constraint
|
|
1047
|
+
* (DROP + ADD). `expr` is trusted SQL emitted verbatim (like policy USING),
|
|
1048
|
+
* `name` is identifier-validated.
|
|
1049
|
+
*/
|
|
1050
|
+
checks?: {
|
|
1051
|
+
name: string;
|
|
1052
|
+
expr: string;
|
|
1053
|
+
}[];
|
|
1054
|
+
/**
|
|
1055
|
+
* Plain (non-unique) btree indexes over an ordered column list, emitted as
|
|
1056
|
+
* standalone `CREATE INDEX [IF NOT EXISTS] name ON table (col1, col2)`
|
|
1057
|
+
* statements (NOT a table clause — a separate migration statement category).
|
|
1058
|
+
* Structural compare by NAME (no expression normalization). `name` and each
|
|
1059
|
+
* column are identifier-validated by the Go differ.
|
|
1060
|
+
*
|
|
1061
|
+
* Scope: columns-only plain btree. Partial (`where`) and expression indexes
|
|
1062
|
+
* are a deliberate follow-up — modelling them needs the same raw-SQL
|
|
1063
|
+
* normalization round-trip CHECK uses (Task 10), so they are NOT in this
|
|
1064
|
+
* type yet to avoid a half-working partial-index path.
|
|
1065
|
+
*/
|
|
1066
|
+
indexes?: (p: PolicyExprCtx<Extract<keyof C, string>>) => (IndexBuilder | IndexDef)[];
|
|
1067
|
+
/**
|
|
1068
|
+
* appendOnly (FR-030): tablo yalnız INSERT kabul eder — UPDATE/DELETE hem
|
|
1069
|
+
* veritabanı yetkisiyle hem üretilen tipte reddedilir (T017/T018).
|
|
1070
|
+
* Ledger deseninin veri katmanından istediği TEK şey budur: bakiye türetilmiş
|
|
1071
|
+
* bir değerdir ve entry log'u değiştirilebilirse doğruluğunu kaybeder.
|
|
1072
|
+
*/
|
|
1073
|
+
appendOnly?: boolean;
|
|
1074
|
+
/** Arama beyanı — bkz. SearchDecl. */
|
|
1075
|
+
search?: SearchDecl;
|
|
1076
|
+
/** Hafıza beyanı — bkz. MemoryDecl (FR-032). */
|
|
1077
|
+
memory?: MemoryDecl;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* A table definition — the runtime value the Go runtime's `schema_extract.js`
|
|
1081
|
+
* reads. It keys tables by `tableDef.name`, reads `tableDef.columns` for the
|
|
1082
|
+
* column DDL, and `tableDef.rls` + `tableDef.policies` for RLS.
|
|
1083
|
+
*
|
|
1084
|
+
* `defineSchema` derives `name` from the object key, so authors never repeat
|
|
1085
|
+
* the table name. `rls`/`policies` are always present after normalization
|
|
1086
|
+
* (defaulted to `true`/`[]`).
|
|
1087
|
+
*
|
|
1088
|
+
* The `C` type parameter preserves the precise per-column phantom types so that
|
|
1089
|
+
* downstream mapped types (InsertShape, RowShape) can discriminate on them.
|
|
1090
|
+
*/
|
|
1091
|
+
interface TableDef<C extends ColumnMap = ColumnMap> {
|
|
1092
|
+
name: string;
|
|
1093
|
+
columns: C;
|
|
1094
|
+
rls: boolean;
|
|
1095
|
+
policies: PolicyDef[];
|
|
1096
|
+
primaryKey?: string[];
|
|
1097
|
+
unique?: {
|
|
1098
|
+
name: string;
|
|
1099
|
+
columns: string[];
|
|
1100
|
+
}[];
|
|
1101
|
+
/** Named raw-SQL DDL objects emitted verbatim on deploy. Tracked by name. */
|
|
1102
|
+
raw?: RawConstraintDef[];
|
|
1103
|
+
/** Named first-class CHECK constraints. Diffed by name + (normalized) body. */
|
|
1104
|
+
checks?: {
|
|
1105
|
+
name: string;
|
|
1106
|
+
expr: string;
|
|
1107
|
+
}[];
|
|
1108
|
+
/** Index bildirimleri; partial/expression/sıra/covering alanlarıyla (FR-041…043). */
|
|
1109
|
+
indexes?: IndexDef[];
|
|
1110
|
+
/** appendOnly (FR-030) — bkz. TableInput. */
|
|
1111
|
+
appendOnly?: boolean;
|
|
1112
|
+
/** Arama beyanı, doğrulanmış ve taşınmış hali. */
|
|
1113
|
+
search?: SearchDecl;
|
|
1114
|
+
/** Hafıza beyanı, doğrulanmış ve taşınmış hali (FR-032). */
|
|
1115
|
+
memory?: MemoryDecl;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* A schema definition containing multiple tables, keyed by table name.
|
|
1119
|
+
*
|
|
1120
|
+
* The `T` type parameter preserves the exact `TableDef<...>` type for each
|
|
1121
|
+
* table so that `SchemaDef["tables"]["rooms"]` resolves to the precise
|
|
1122
|
+
* `TableDef<{ id: ColumnBuilder<'uuid', false, true, never>; ... }>`.
|
|
1123
|
+
*/
|
|
1124
|
+
interface SchemaDef<T extends Record<string, TableDef> = Record<string, TableDef>> {
|
|
1125
|
+
/** The schema's own name — `public`, `billing`, … Declared, never derived. */
|
|
1126
|
+
name: string;
|
|
1127
|
+
/** Reachable over HTTP? False unless the author opts in. */
|
|
1128
|
+
exposed: boolean;
|
|
1129
|
+
tables: T;
|
|
1130
|
+
/** Postgres extensions to install on deploy. Normalized to `[]` when absent. */
|
|
1131
|
+
extensions: PalbaseExtension[];
|
|
1132
|
+
}
|
|
1133
|
+
/** Map the author's `{ tables: { <name>: { columns } } }` input to the
|
|
1134
|
+
* `{ tables: { <name>: TableDef<columns> } }` runtime/type shape, threading the
|
|
1135
|
+
* per-table column map `T[K]["columns"]` so column-level inference survives. */
|
|
1136
|
+
type TablesByName<T extends readonly TableHandle[]> = {
|
|
1137
|
+
[K in T[number] as K[typeof TABLE_META]["name"]]: K[typeof TABLE_META];
|
|
1138
|
+
};
|
|
1139
|
+
/**
|
|
1140
|
+
* How a schema file reads. One file per schema — `db/public.ts`, `db/billing.ts`
|
|
1141
|
+
* — and the schema says its own name:
|
|
1142
|
+
*
|
|
1143
|
+
* const todos = defineTable("todos", {
|
|
1144
|
+
* columns: {
|
|
1145
|
+
* id: uuid().primaryKey().defaultRandom(),
|
|
1146
|
+
* owner: text().notNull(),
|
|
1147
|
+
* title: text().notNull(),
|
|
1148
|
+
* },
|
|
1149
|
+
* rls: true,
|
|
1150
|
+
* policies: [
|
|
1151
|
+
* policy("owner_all").for("all").to("authenticated")
|
|
1152
|
+
* .using("owner = (select auth.uid())")
|
|
1153
|
+
* .withCheck("owner = (select auth.uid())"),
|
|
1154
|
+
* ],
|
|
1155
|
+
* });
|
|
1156
|
+
*
|
|
1157
|
+
* export default defineSchema("public", { tables: [todos] });
|
|
1158
|
+
*
|
|
1159
|
+
* The dictionary form `defineSchema({ tables: { todos: {…} } })` is RETIRED and
|
|
1160
|
+
* refused by name: a table whose name comes from a key does not know what it is
|
|
1161
|
+
* called while it is being built, so it cannot resolve `references(() =>
|
|
1162
|
+
* other.id)`, and a schema that does not say its own name cannot tell one
|
|
1163
|
+
* `invoices` from another schema's `invoices`.
|
|
1164
|
+
*
|
|
1165
|
+
* The returned value is `{ name, tables: { todos: { name, columns, rls,
|
|
1166
|
+
* policies } }, extensions, exposed }` — the exact shape the runtime schema
|
|
1167
|
+
* extractor parses. Per-column phantom types are preserved so
|
|
1168
|
+
* `Database.public.todos.insert({...})` stays typed.
|
|
1169
|
+
*
|
|
1170
|
+
* RLS normalization: `rls` defaults to **`true`**, `policies` to `[]`. A table
|
|
1171
|
+
* that declares neither is therefore deny-all — nothing reads it until a policy
|
|
1172
|
+
* says who may, which is the safe starting point rather than a bug. Declare
|
|
1173
|
+
* `rls: false` for a genuinely public table; that is an explicit, greppable
|
|
1174
|
+
* statement of intent instead of an omission. When `policies` is non-empty,
|
|
1175
|
+
* `rls` is forced on (ENABLE + FORCE) regardless of the declared flag — a table
|
|
1176
|
+
* with policies must have RLS enabled or the policies would be inert.
|
|
1177
|
+
*/
|
|
1178
|
+
/**
|
|
1179
|
+
* Where a table's metadata lives.
|
|
1180
|
+
*
|
|
1181
|
+
* The table VALUE is its columns, so `lists.id` is the column — which means a
|
|
1182
|
+
* column called `name` would overwrite the table's own name if metadata sat in
|
|
1183
|
+
* a plain field. Measured, not imagined: the existing `users` / `rooms` tests
|
|
1184
|
+
* (both declare a `name` column) produced an `[object Object]` table key.
|
|
1185
|
+
* `columns`, `rls`, `search`, `memory`, `unique` and `indexes` are all plausible
|
|
1186
|
+
* column names too. A column cannot collide with a symbol.
|
|
1187
|
+
*/
|
|
1188
|
+
declare const TABLE_META: unique symbol;
|
|
1189
|
+
/** A value produced by `defineTable`: the columns, plus metadata behind the symbol. */
|
|
1190
|
+
type TableHandle<C extends ColumnMap = ColumnMap, N extends string = string> = C & {
|
|
1191
|
+
readonly [TABLE_META]: TableDef<C> & {
|
|
1192
|
+
name: N;
|
|
1193
|
+
};
|
|
1194
|
+
};
|
|
1195
|
+
/**
|
|
1196
|
+
* Declare one table as a standalone value that KNOWS ITS OWN NAME.
|
|
1197
|
+
*
|
|
1198
|
+
* The name lives here, not in a dictionary key. A table whose name comes from a
|
|
1199
|
+
* key cannot resolve `references(() => other.id)` when it is built — it does not
|
|
1200
|
+
* know what to call itself — which forces a two-phase resolution with an
|
|
1201
|
+
* empty-name intermediate state. One name source, no empty identity.
|
|
1202
|
+
*
|
|
1203
|
+
* The returned object spreads its own columns, so a sibling can point at it:
|
|
1204
|
+
* `references(() => lists.id)`.
|
|
1205
|
+
*/
|
|
1206
|
+
declare function defineTable<const N extends string, C extends ColumnMap>(name: N, input: TableInput<C>): TableHandle<C, N>;
|
|
1207
|
+
/**
|
|
1208
|
+
* Collect declared tables into one schema.
|
|
1209
|
+
*
|
|
1210
|
+
* Takes an ARRAY, not a dictionary: the tables already carry their names, and a
|
|
1211
|
+
* second name source is a second thing that can disagree.
|
|
1212
|
+
*/
|
|
1213
|
+
declare function defineSchema<const T extends readonly TableHandle[]>(name: string, input: {
|
|
1214
|
+
tables: T;
|
|
1215
|
+
exposed?: boolean;
|
|
1216
|
+
extensions?: PalbaseExtension[];
|
|
1217
|
+
}): SchemaDef<TablesByName<T>>;
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Local typed interfaces for the 9 Palbase module clients injected into
|
|
1221
|
+
* every endpoint context via `ctx.auth`, `ctx.storage`, etc.
|
|
1222
|
+
*
|
|
1223
|
+
* Design intent
|
|
1224
|
+
* ─────────────
|
|
1225
|
+
* • @palbase/backend owns this contract — no runtime or type dependency on
|
|
1226
|
+
* @palbase/server or any module package.
|
|
1227
|
+
* • These interfaces are STRUCTURALLY compatible with the runtime's
|
|
1228
|
+
* ServerClient module objects so assignment is valid without a cast.
|
|
1229
|
+
* • All parameter and return types are defined LOCALLY here (not imported
|
|
1230
|
+
* from modules/) to keep the package self-contained.
|
|
1231
|
+
*
|
|
1232
|
+
* Privilege note
|
|
1233
|
+
* ──────────────
|
|
1234
|
+
* These clients run with the project's managed-runtime (privileged) key —
|
|
1235
|
+
* they bypass end-user RLS; intentional for server handlers. Treat every
|
|
1236
|
+
* call as if it has admin access to the project's data.
|
|
1237
|
+
*/
|
|
1238
|
+
|
|
1239
|
+
/** A user as returned by auth verifyUserToken. */
|
|
1240
|
+
interface PalbaseUser {
|
|
1241
|
+
id: string;
|
|
1242
|
+
email: string;
|
|
1243
|
+
emailVerified?: boolean;
|
|
1244
|
+
createdAt?: string;
|
|
1245
|
+
updatedAt?: string;
|
|
1246
|
+
metadata?: Record<string, unknown>;
|
|
1247
|
+
}
|
|
1248
|
+
/** A session as returned by auth.getSession(). */
|
|
1249
|
+
interface PalbaseSession {
|
|
1250
|
+
accessToken: string;
|
|
1251
|
+
refreshToken: string;
|
|
1252
|
+
expiresAt: number;
|
|
1253
|
+
}
|
|
1254
|
+
/** MFA enroll result. */
|
|
1255
|
+
interface PalbaseMFAEnrollResult {
|
|
1256
|
+
enrollment_id?: string;
|
|
1257
|
+
secret?: string;
|
|
1258
|
+
otp_url?: string;
|
|
1259
|
+
qr_code?: string;
|
|
1260
|
+
recovery_codes?: string[];
|
|
1261
|
+
status?: string;
|
|
1262
|
+
}
|
|
1263
|
+
/** MFA token response. */
|
|
1264
|
+
interface PalbaseTokenResponse {
|
|
1265
|
+
access_token: string;
|
|
1266
|
+
refresh_token: string;
|
|
1267
|
+
token_type: string;
|
|
1268
|
+
expires_in: number;
|
|
1269
|
+
}
|
|
1270
|
+
/** MFA factor. */
|
|
1271
|
+
interface PalbaseMFAFactor {
|
|
1272
|
+
id: string;
|
|
1273
|
+
type: string;
|
|
1274
|
+
verified: boolean;
|
|
1275
|
+
created_at: string;
|
|
1276
|
+
}
|
|
1277
|
+
/** Device token view returned by registerDevice. */
|
|
1278
|
+
interface PalbaseDeviceTokenView {
|
|
1279
|
+
id: string;
|
|
1280
|
+
device_id: string;
|
|
1281
|
+
platform: "android" | "ios" | "web";
|
|
1282
|
+
status: "active" | "inactive";
|
|
1283
|
+
created_at: string;
|
|
1284
|
+
updated_at: string;
|
|
1285
|
+
}
|
|
1286
|
+
/** Device info item returned by device.list(). */
|
|
1287
|
+
interface PalbaseDeviceInfo {
|
|
1288
|
+
id: string;
|
|
1289
|
+
platform: string;
|
|
1290
|
+
attestation_status: string;
|
|
1291
|
+
bound: boolean;
|
|
1292
|
+
created_at: string;
|
|
1293
|
+
}
|
|
1294
|
+
/** Params for device.attestAndroid(). */
|
|
1295
|
+
interface PalbaseAttestAndroidParams {
|
|
1296
|
+
verdict_token: string;
|
|
1297
|
+
}
|
|
1298
|
+
/** Result of device.attestAndroid(). */
|
|
1299
|
+
interface PalbaseAttestAndroidResult {
|
|
1300
|
+
device_id: string;
|
|
1301
|
+
attestation_status: string;
|
|
1302
|
+
device_integrity?: string;
|
|
1303
|
+
}
|
|
1304
|
+
/** Params for device.attestiOS(). */
|
|
1305
|
+
interface PalbaseAttestiOSParams {
|
|
1306
|
+
attestation_object: string;
|
|
1307
|
+
key_id: string;
|
|
1308
|
+
challenge: string;
|
|
1309
|
+
}
|
|
1310
|
+
/** Result of device.attestiOS(). */
|
|
1311
|
+
interface PalbaseAttestiOSResult {
|
|
1312
|
+
device_id: string;
|
|
1313
|
+
attestation_status: string;
|
|
1314
|
+
}
|
|
1315
|
+
/** Params for device.bind(). */
|
|
1316
|
+
interface PalbaseBindDeviceParams {
|
|
1317
|
+
device_id: string;
|
|
1318
|
+
public_key: string;
|
|
1319
|
+
platform_attestation?: string;
|
|
1320
|
+
}
|
|
1321
|
+
/** Params for device.verifyRequestSignature() (server-only). */
|
|
1322
|
+
interface PalbaseVerifyRequestSignatureParams {
|
|
1323
|
+
payload: string;
|
|
1324
|
+
signature: string;
|
|
1325
|
+
}
|
|
1326
|
+
/** Flag context (user targeting). */
|
|
1327
|
+
interface PalbaseFlagContext {
|
|
1328
|
+
userId?: string;
|
|
1329
|
+
properties?: Record<string, unknown>;
|
|
1330
|
+
}
|
|
1331
|
+
/** Feature flag variant. */
|
|
1332
|
+
interface PalbaseFlagVariant {
|
|
1333
|
+
name: string;
|
|
1334
|
+
payload?: unknown;
|
|
1335
|
+
}
|
|
1336
|
+
/** Feature flag (getAll result item). */
|
|
1337
|
+
interface PalbaseFlag {
|
|
1338
|
+
name: string;
|
|
1339
|
+
enabled: boolean;
|
|
1340
|
+
variant?: PalbaseFlagVariant;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Any JSON value a feature flag (or per-user override) can hold once resolved.
|
|
1344
|
+
* Mirrors the wire shape the user-flags module stores and returns.
|
|
1345
|
+
*/
|
|
1346
|
+
type PalbaseFlagValue = boolean | number | string | null | PalbaseFlagValue[] | {
|
|
1347
|
+
[key: string]: PalbaseFlagValue;
|
|
1348
|
+
};
|
|
1349
|
+
/** Where a resolved flag value came from. */
|
|
1350
|
+
type PalbaseFlagSource = "system" | "user";
|
|
1351
|
+
/** Result of {@link PalbaseFlagsClient.setOverride} /
|
|
1352
|
+
* {@link PalbaseFlagsServiceClient.setOverrideForUser}. */
|
|
1353
|
+
interface PalbaseSetOverrideResult {
|
|
1354
|
+
key: string;
|
|
1355
|
+
value: PalbaseFlagValue;
|
|
1356
|
+
source: PalbaseFlagSource;
|
|
1357
|
+
}
|
|
1358
|
+
/** Result of {@link PalbaseFlagsServiceClient.setOverridesForUser}. */
|
|
1359
|
+
interface PalbaseSetOverridesResult {
|
|
1360
|
+
values: Record<string, PalbaseFlagValue>;
|
|
1361
|
+
}
|
|
1362
|
+
/** Result of {@link PalbaseFlagsServiceClient.clearOverrideForUser} — `value`
|
|
1363
|
+
* is the system default the user falls back to. */
|
|
1364
|
+
interface PalbaseClearOverrideResult {
|
|
1365
|
+
key: string;
|
|
1366
|
+
value: PalbaseFlagValue;
|
|
1367
|
+
source: PalbaseFlagSource;
|
|
1368
|
+
}
|
|
1369
|
+
/** Result of {@link PalbaseFlagsServiceClient.clearAllOverridesForUser}. */
|
|
1370
|
+
interface PalbaseClearAllOverridesResult {
|
|
1371
|
+
deleted: number;
|
|
1372
|
+
}
|
|
1373
|
+
/** One cross-user operation for {@link PalbaseFlagsServiceClient.batchSetOverrides}. */
|
|
1374
|
+
interface PalbaseBatchOverrideOperation {
|
|
1375
|
+
userId: string;
|
|
1376
|
+
values: Record<string, PalbaseFlagValue>;
|
|
1377
|
+
}
|
|
1378
|
+
/** Result of {@link PalbaseFlagsServiceClient.batchSetOverrides}. */
|
|
1379
|
+
interface PalbaseBatchSetOverridesResult {
|
|
1380
|
+
applied: number;
|
|
1381
|
+
}
|
|
1382
|
+
/** File object returned by storage operations. */
|
|
1383
|
+
interface PalbaseFileObject {
|
|
1384
|
+
/** The object's path inside its bucket. `name` is the same value. */
|
|
1385
|
+
path: string;
|
|
1386
|
+
name: string;
|
|
1387
|
+
bucket: string;
|
|
1388
|
+
size: number;
|
|
1389
|
+
contentType: string;
|
|
1390
|
+
/** SHA-256 of the stored bytes, computed as they were written. */
|
|
1391
|
+
checksum: string;
|
|
1392
|
+
/** Present for images: the decoded dimensions of the ORIGINAL. */
|
|
1393
|
+
width?: number;
|
|
1394
|
+
height?: number;
|
|
1395
|
+
/**
|
|
1396
|
+
* A ThumbHash — a few dozen bytes that render as a blurred preview while the
|
|
1397
|
+
* real image loads, with no second request to fetch it.
|
|
1398
|
+
*/
|
|
1399
|
+
thumbhash?: string;
|
|
1400
|
+
/**
|
|
1401
|
+
* Declared rendition name → the URL that serves it. Produced at upload from
|
|
1402
|
+
* what `config/storage.ts` declared, so this is a lookup rather than a
|
|
1403
|
+
* transform anyone pays for per request.
|
|
1404
|
+
*/
|
|
1405
|
+
variants: Record<string, string>;
|
|
1406
|
+
}
|
|
1407
|
+
/** Storage signed URL response. */
|
|
1408
|
+
interface PalbaseSignedUrlResponse {
|
|
1409
|
+
signedUrl: string;
|
|
1410
|
+
}
|
|
1411
|
+
/** Storage upload options. */
|
|
1412
|
+
interface PalbaseUploadOptions {
|
|
1413
|
+
contentType?: string;
|
|
1414
|
+
upsert?: boolean;
|
|
1415
|
+
}
|
|
1416
|
+
/** Storage transform options for public URLs. */
|
|
1417
|
+
interface PalbaseTransformOptions {
|
|
1418
|
+
width?: number;
|
|
1419
|
+
height?: number;
|
|
1420
|
+
format?: "webp" | "avif" | "jpeg" | "png";
|
|
1421
|
+
}
|
|
1422
|
+
/** Storage list options. */
|
|
1423
|
+
interface PalbaseListOptions {
|
|
1424
|
+
limit?: number;
|
|
1425
|
+
offset?: number;
|
|
1426
|
+
sortBy?: {
|
|
1427
|
+
column: string;
|
|
1428
|
+
order: "asc" | "desc";
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
/** Edge-function invoke options. */
|
|
1432
|
+
interface PalbaseInvokeOptions {
|
|
1433
|
+
body?: unknown;
|
|
1434
|
+
headers?: Record<string, string>;
|
|
1435
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
1436
|
+
}
|
|
1437
|
+
/** Push recipient — user ID, array of user IDs, or a topic. */
|
|
1438
|
+
type PalbasePushRecipient = string | string[] | {
|
|
1439
|
+
topic: string;
|
|
1440
|
+
};
|
|
1441
|
+
/** Localized text (plain or locale map). */
|
|
1442
|
+
type PalbaseLocalizedText = string | Record<string, string>;
|
|
1443
|
+
/** Push send params. */
|
|
1444
|
+
interface PalbasePushSendParams {
|
|
1445
|
+
to: PalbasePushRecipient;
|
|
1446
|
+
title?: PalbaseLocalizedText;
|
|
1447
|
+
body?: PalbaseLocalizedText;
|
|
1448
|
+
variables?: Record<string, string>;
|
|
1449
|
+
default_locale?: string;
|
|
1450
|
+
data?: Record<string, string>;
|
|
1451
|
+
image?: string;
|
|
1452
|
+
badge?: number;
|
|
1453
|
+
sound?: string;
|
|
1454
|
+
deep_link?: string;
|
|
1455
|
+
collapse_key?: string;
|
|
1456
|
+
priority?: "high" | "normal";
|
|
1457
|
+
ttl?: number;
|
|
1458
|
+
silent?: boolean;
|
|
1459
|
+
content_available?: boolean;
|
|
1460
|
+
category?: string;
|
|
1461
|
+
metadata?: unknown;
|
|
1462
|
+
channels?: Array<"push" | "inbox">;
|
|
1463
|
+
inbox_action_url?: string;
|
|
1464
|
+
}
|
|
1465
|
+
/** Push send response. */
|
|
1466
|
+
interface PalbasePushSendResponse {
|
|
1467
|
+
message_id?: string;
|
|
1468
|
+
message_ids?: string[];
|
|
1469
|
+
recipients: number;
|
|
1470
|
+
}
|
|
1471
|
+
/** Email send params. */
|
|
1472
|
+
interface PalbaseEmailSendParams {
|
|
1473
|
+
to: string | string[];
|
|
1474
|
+
subject?: string;
|
|
1475
|
+
/**
|
|
1476
|
+
* @deprecated The server reads `template_slug`, not `template` — this
|
|
1477
|
+
* field is forwarded as-is and ignored by palnotify. Use `templateSlug`
|
|
1478
|
+
* instead. Retained for source-compat with older callers.
|
|
1479
|
+
*/
|
|
1480
|
+
template?: string;
|
|
1481
|
+
/**
|
|
1482
|
+
* Server email-template slug. The notifications client maps this to the
|
|
1483
|
+
* wire field `template_slug` (mutually exclusive with `subject` + body
|
|
1484
|
+
* fields when not overriding template output).
|
|
1485
|
+
*/
|
|
1486
|
+
templateSlug?: string;
|
|
1487
|
+
/**
|
|
1488
|
+
* Which locale of `templateSlug` to render — a BCP47 short tag ("tr").
|
|
1489
|
+
* Falls back to "en" when the slug has no row for it. Declaring a locale in
|
|
1490
|
+
* config/notifications.ts is only half the feature: without this the extra
|
|
1491
|
+
* rows exist and are unreachable.
|
|
1492
|
+
*/
|
|
1493
|
+
locale?: string;
|
|
1494
|
+
variables?: Record<string, unknown>;
|
|
1495
|
+
html?: string;
|
|
1496
|
+
text?: string;
|
|
1497
|
+
from?: {
|
|
1498
|
+
email: string;
|
|
1499
|
+
name?: string;
|
|
1500
|
+
};
|
|
1501
|
+
reply_to?: string;
|
|
1502
|
+
category?: string;
|
|
1503
|
+
}
|
|
1504
|
+
/** Email send response. */
|
|
1505
|
+
interface PalbaseEmailSendResponse {
|
|
1506
|
+
message_id?: string;
|
|
1507
|
+
message_ids?: string[];
|
|
1508
|
+
}
|
|
1509
|
+
/** SMS send params. */
|
|
1510
|
+
interface PalbaseSmsSendParams {
|
|
1511
|
+
to: string | string[];
|
|
1512
|
+
/**
|
|
1513
|
+
* SMS body text. Either `body` OR `templateSlug` must be supplied —
|
|
1514
|
+
* the two are mutually exclusive at the server.
|
|
1515
|
+
*/
|
|
1516
|
+
body?: string;
|
|
1517
|
+
/**
|
|
1518
|
+
* Server SMS-template slug. The notifications client maps this to the
|
|
1519
|
+
* wire field `template_slug`. When provided, palnotify renders the
|
|
1520
|
+
* template with `variables` and uses the result as the SMS body.
|
|
1521
|
+
*/
|
|
1522
|
+
templateSlug?: string;
|
|
1523
|
+
/** Which locale of `templateSlug` to render — see PalbaseEmailSendParams.locale. */
|
|
1524
|
+
locale?: string;
|
|
1525
|
+
variables?: Record<string, unknown>;
|
|
1526
|
+
category?: string;
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Phone-verification start parameters.
|
|
1530
|
+
*
|
|
1531
|
+
* There is no body field, by construction: the provider generates the code AND
|
|
1532
|
+
* the message text from its own template. A caller-supplied body would be
|
|
1533
|
+
* silently dropped — a send that reports success while delivering something the
|
|
1534
|
+
* caller never wrote.
|
|
1535
|
+
*/
|
|
1536
|
+
interface PalbaseVerificationStartParams {
|
|
1537
|
+
/** E.164 recipient, e.g. "+14155551212". */
|
|
1538
|
+
to: string;
|
|
1539
|
+
/** Template language (e.g. "tr", "en"). Empty uses the service default. */
|
|
1540
|
+
locale?: string;
|
|
1541
|
+
/** Optional user attribution for the message log. */
|
|
1542
|
+
user_id?: string;
|
|
1543
|
+
}
|
|
1544
|
+
/** Phone-verification start response. The code is never included. */
|
|
1545
|
+
interface PalbaseVerificationStartResponse {
|
|
1546
|
+
/** Provider-side handle for this attempt. Not a secret, and not the code. */
|
|
1547
|
+
verification_sid: string;
|
|
1548
|
+
/** Provider lifecycle value, typically "pending". */
|
|
1549
|
+
status: string;
|
|
1550
|
+
to: string;
|
|
1551
|
+
/** palnotify message-log row id, so the send appears in message history. */
|
|
1552
|
+
message_id?: string;
|
|
1553
|
+
}
|
|
1554
|
+
/** Phone-verification check parameters. */
|
|
1555
|
+
interface PalbaseVerificationCheckParams {
|
|
1556
|
+
to: string;
|
|
1557
|
+
code: string;
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Phone-verification verdict. A wrong or expired code is `approved: false` with
|
|
1561
|
+
* a successful HTTP status — a mismatch is a normal outcome, not an error.
|
|
1562
|
+
*/
|
|
1563
|
+
interface PalbaseVerificationCheckResponse {
|
|
1564
|
+
approved: boolean;
|
|
1565
|
+
}
|
|
1566
|
+
/** SMS send response. */
|
|
1567
|
+
interface PalbaseSmsSendResponse {
|
|
1568
|
+
message_id?: string;
|
|
1569
|
+
message_ids?: string[];
|
|
1570
|
+
}
|
|
1571
|
+
/** Inbox send params (managed-runtime: create inbox row for a user). */
|
|
1572
|
+
interface PalbaseInboxSendParams {
|
|
1573
|
+
to: string;
|
|
1574
|
+
title?: string;
|
|
1575
|
+
body: string;
|
|
1576
|
+
data?: unknown;
|
|
1577
|
+
action_url?: string;
|
|
1578
|
+
category?: string;
|
|
1579
|
+
channels?: Array<"push" | "inbox">;
|
|
1580
|
+
push_deep_link?: string;
|
|
1581
|
+
}
|
|
1582
|
+
/** Inbox send response. */
|
|
1583
|
+
interface PalbaseInboxSendResponse {
|
|
1584
|
+
message_id?: string;
|
|
1585
|
+
skipped?: boolean;
|
|
1586
|
+
}
|
|
1587
|
+
/** A per-channel status entry in a multi-channel response. */
|
|
1588
|
+
interface PalbaseChannelOutcome {
|
|
1589
|
+
status: "queued" | "sent" | "skipped" | "failed";
|
|
1590
|
+
message_id?: string;
|
|
1591
|
+
message_ids?: string[];
|
|
1592
|
+
recipients?: number;
|
|
1593
|
+
error?: string;
|
|
1594
|
+
}
|
|
1595
|
+
/** Multi-channel fan-out response. */
|
|
1596
|
+
interface PalbaseMultiChannelResponse {
|
|
1597
|
+
channels: Record<string, PalbaseChannelOutcome>;
|
|
1598
|
+
}
|
|
1599
|
+
/** An inbox message as returned by inbox.list. */
|
|
1600
|
+
interface PalbaseInboxMessage {
|
|
1601
|
+
id: string;
|
|
1602
|
+
user_id?: string;
|
|
1603
|
+
title?: string;
|
|
1604
|
+
body: string;
|
|
1605
|
+
data?: unknown;
|
|
1606
|
+
action_url?: string;
|
|
1607
|
+
category?: string;
|
|
1608
|
+
is_read: boolean;
|
|
1609
|
+
read_at?: string;
|
|
1610
|
+
created_at: string;
|
|
1611
|
+
}
|
|
1612
|
+
/** Inbox list options. */
|
|
1613
|
+
interface PalbaseInboxListOptions {
|
|
1614
|
+
cursor?: string;
|
|
1615
|
+
limit?: number;
|
|
1616
|
+
is_read?: boolean;
|
|
1617
|
+
category?: string;
|
|
1618
|
+
include_archived?: boolean;
|
|
1619
|
+
}
|
|
1620
|
+
/** Inbox list result. */
|
|
1621
|
+
interface PalbaseInboxListResult {
|
|
1622
|
+
messages: PalbaseInboxMessage[];
|
|
1623
|
+
next_cursor?: string;
|
|
1624
|
+
}
|
|
1625
|
+
/** Notification preferences (channel × category opt-in/out). */
|
|
1626
|
+
type PalbasePreferences = Partial<Record<"push" | "email" | "sms" | "inbox", Record<string, boolean>>>;
|
|
1627
|
+
/** Register device params. */
|
|
1628
|
+
interface PalbaseRegisterDeviceParams {
|
|
1629
|
+
device_id: string;
|
|
1630
|
+
token: string;
|
|
1631
|
+
platform: "android" | "ios" | "web";
|
|
1632
|
+
app_version?: string;
|
|
1633
|
+
locale?: string;
|
|
1634
|
+
}
|
|
1635
|
+
/** Email template view (camelCase). */
|
|
1636
|
+
interface PalbaseEmailTemplate {
|
|
1637
|
+
id: string;
|
|
1638
|
+
slug: string;
|
|
1639
|
+
locale: string;
|
|
1640
|
+
subject: string;
|
|
1641
|
+
htmlBody: string;
|
|
1642
|
+
textBody?: string;
|
|
1643
|
+
variables: string[];
|
|
1644
|
+
isDefault: boolean;
|
|
1645
|
+
createdAt: string;
|
|
1646
|
+
updatedAt: string;
|
|
1647
|
+
}
|
|
1648
|
+
/** Payload for templates.email.create. */
|
|
1649
|
+
interface PalbaseCreateEmailTemplateInput {
|
|
1650
|
+
slug: string;
|
|
1651
|
+
locale?: string;
|
|
1652
|
+
subject: string;
|
|
1653
|
+
htmlBody: string;
|
|
1654
|
+
textBody?: string;
|
|
1655
|
+
variables?: string[];
|
|
1656
|
+
}
|
|
1657
|
+
/** Payload for templates.email.update — patch semantics. */
|
|
1658
|
+
interface PalbaseUpdateEmailTemplateInput {
|
|
1659
|
+
subject?: string;
|
|
1660
|
+
htmlBody?: string;
|
|
1661
|
+
textBody?: string;
|
|
1662
|
+
variables?: string[];
|
|
1663
|
+
}
|
|
1664
|
+
/** SMS template view (camelCase). No subject / html / text distinction. */
|
|
1665
|
+
interface PalbaseSMSTemplate {
|
|
1666
|
+
id: string;
|
|
1667
|
+
slug: string;
|
|
1668
|
+
locale: string;
|
|
1669
|
+
body: string;
|
|
1670
|
+
variables: string[];
|
|
1671
|
+
isDefault: boolean;
|
|
1672
|
+
createdAt: string;
|
|
1673
|
+
updatedAt: string;
|
|
1674
|
+
}
|
|
1675
|
+
/** Payload for templates.sms.create. */
|
|
1676
|
+
interface PalbaseCreateSMSTemplateInput {
|
|
1677
|
+
slug: string;
|
|
1678
|
+
locale?: string;
|
|
1679
|
+
body: string;
|
|
1680
|
+
variables?: string[];
|
|
1681
|
+
}
|
|
1682
|
+
/** Payload for templates.sms.update — patch semantics. */
|
|
1683
|
+
interface PalbaseUpdateSMSTemplateInput {
|
|
1684
|
+
body?: string;
|
|
1685
|
+
variables?: string[];
|
|
1686
|
+
}
|
|
1687
|
+
/** Analytics event properties. */
|
|
1688
|
+
type PalbaseAnalyticsProperties = Record<string, unknown>;
|
|
1689
|
+
/** Analytics identify traits. */
|
|
1690
|
+
type PalbaseIdentifyTraits = Record<string, unknown>;
|
|
1691
|
+
/** Analytics count query input. */
|
|
1692
|
+
interface PalbaseCountQueryInput {
|
|
1693
|
+
eventName?: string;
|
|
1694
|
+
eventNames?: string[];
|
|
1695
|
+
from: number;
|
|
1696
|
+
to: number;
|
|
1697
|
+
interval?: "hour" | "day";
|
|
1698
|
+
filters?: Record<string, string>;
|
|
1699
|
+
breakdown?: string;
|
|
1700
|
+
}
|
|
1701
|
+
/** A single time-bucket in a count result. */
|
|
1702
|
+
interface PalbaseCountBucket {
|
|
1703
|
+
t: number;
|
|
1704
|
+
count: number;
|
|
1705
|
+
breakdown?: string;
|
|
1706
|
+
}
|
|
1707
|
+
/** Count query result. */
|
|
1708
|
+
interface PalbaseCountResult {
|
|
1709
|
+
series: PalbaseCountBucket[];
|
|
1710
|
+
from_mv: boolean;
|
|
1711
|
+
}
|
|
1712
|
+
/** Events query input. */
|
|
1713
|
+
interface PalbaseEventsQueryInput {
|
|
1714
|
+
from: number;
|
|
1715
|
+
to: number;
|
|
1716
|
+
eventName?: string;
|
|
1717
|
+
distinctId?: string;
|
|
1718
|
+
limit?: number;
|
|
1719
|
+
cursor?: string;
|
|
1720
|
+
}
|
|
1721
|
+
/** A single event row. */
|
|
1722
|
+
interface PalbaseEventRow {
|
|
1723
|
+
event_id: string;
|
|
1724
|
+
timestamp: number;
|
|
1725
|
+
event_name: string;
|
|
1726
|
+
distinct_id: string;
|
|
1727
|
+
properties: Record<string, unknown>;
|
|
1728
|
+
}
|
|
1729
|
+
/** Events query result. */
|
|
1730
|
+
interface PalbaseEventsResult {
|
|
1731
|
+
events: PalbaseEventRow[];
|
|
1732
|
+
next_cursor?: string;
|
|
1733
|
+
}
|
|
1734
|
+
/** Properties query input. */
|
|
1735
|
+
interface PalbasePropertiesQueryInput {
|
|
1736
|
+
eventName?: string;
|
|
1737
|
+
from?: number;
|
|
1738
|
+
to?: number;
|
|
1739
|
+
}
|
|
1740
|
+
/** Property descriptor (from /query/properties). */
|
|
1741
|
+
interface PalbasePropertyDescriptor {
|
|
1742
|
+
name: string;
|
|
1743
|
+
value_count: number;
|
|
1744
|
+
}
|
|
1745
|
+
/** Users query input. */
|
|
1746
|
+
interface PalbaseUsersQueryInput {
|
|
1747
|
+
from: number;
|
|
1748
|
+
to: number;
|
|
1749
|
+
filters?: Record<string, string>;
|
|
1750
|
+
limit?: number;
|
|
1751
|
+
cursor?: string;
|
|
1752
|
+
}
|
|
1753
|
+
/** A single user row. */
|
|
1754
|
+
interface PalbaseUserRow {
|
|
1755
|
+
distinct_id: string;
|
|
1756
|
+
first_seen: number;
|
|
1757
|
+
last_seen: number;
|
|
1758
|
+
event_count: number;
|
|
1759
|
+
properties?: Record<string, unknown>;
|
|
1760
|
+
}
|
|
1761
|
+
/** Users query result. */
|
|
1762
|
+
interface PalbaseUsersResult {
|
|
1763
|
+
users: PalbaseUserRow[];
|
|
1764
|
+
next_cursor?: string;
|
|
1765
|
+
}
|
|
1766
|
+
/** Funnel query input. */
|
|
1767
|
+
interface PalbaseFunnelQueryInput {
|
|
1768
|
+
steps: Array<{
|
|
1769
|
+
event_name: string;
|
|
1770
|
+
filters?: Record<string, string>;
|
|
1771
|
+
}>;
|
|
1772
|
+
from: number;
|
|
1773
|
+
to: number;
|
|
1774
|
+
conversionWindowSeconds?: number;
|
|
1775
|
+
breakdown?: string;
|
|
1776
|
+
}
|
|
1777
|
+
/** Funnel step result. */
|
|
1778
|
+
interface PalbaseFunnelStepResult {
|
|
1779
|
+
event_name: string;
|
|
1780
|
+
count: number;
|
|
1781
|
+
conversion_rate: number;
|
|
1782
|
+
}
|
|
1783
|
+
/** Funnel query result. */
|
|
1784
|
+
interface PalbaseFunnelResult {
|
|
1785
|
+
steps: PalbaseFunnelStepResult[];
|
|
1786
|
+
overall_conversion_rate: number;
|
|
1787
|
+
}
|
|
1788
|
+
/** Retention query input. */
|
|
1789
|
+
interface PalbaseRetentionQueryInput {
|
|
1790
|
+
firstEvent: string;
|
|
1791
|
+
returnEvent: string;
|
|
1792
|
+
from: number;
|
|
1793
|
+
to: number;
|
|
1794
|
+
periodDays?: number;
|
|
1795
|
+
periods?: number;
|
|
1796
|
+
}
|
|
1797
|
+
/** Retention cohort result item. */
|
|
1798
|
+
interface PalbaseRetentionCohort {
|
|
1799
|
+
cohort_start: number;
|
|
1800
|
+
size: number;
|
|
1801
|
+
periods: number[];
|
|
1802
|
+
}
|
|
1803
|
+
/** Retention query result. */
|
|
1804
|
+
interface PalbaseRetentionResult {
|
|
1805
|
+
cohorts: PalbaseRetentionCohort[];
|
|
1806
|
+
}
|
|
1807
|
+
/** Cohort query input. */
|
|
1808
|
+
interface PalbaseCohortQueryInput {
|
|
1809
|
+
name: string;
|
|
1810
|
+
rules: Array<{
|
|
1811
|
+
event_name: string;
|
|
1812
|
+
filters?: Record<string, string>;
|
|
1813
|
+
}>;
|
|
1814
|
+
from: number;
|
|
1815
|
+
to: number;
|
|
1816
|
+
}
|
|
1817
|
+
/** Cohort query result. */
|
|
1818
|
+
interface PalbaseCohortResult {
|
|
1819
|
+
size: number;
|
|
1820
|
+
distinct_ids_sample: string[];
|
|
1821
|
+
}
|
|
1822
|
+
/** Analytics overview result. */
|
|
1823
|
+
interface PalbaseOverviewResult {
|
|
1824
|
+
dau: number;
|
|
1825
|
+
wau: number;
|
|
1826
|
+
mau: number;
|
|
1827
|
+
total_events: number;
|
|
1828
|
+
top_events: Array<{
|
|
1829
|
+
event_name: string;
|
|
1830
|
+
count: number;
|
|
1831
|
+
}>;
|
|
1832
|
+
}
|
|
1833
|
+
/** Event names result. */
|
|
1834
|
+
interface PalbaseEventNamesResult {
|
|
1835
|
+
names: string[];
|
|
1836
|
+
}
|
|
1837
|
+
/** User detail result. */
|
|
1838
|
+
interface PalbaseUserDetailResult {
|
|
1839
|
+
distinct_id: string;
|
|
1840
|
+
first_seen: number;
|
|
1841
|
+
last_seen: number;
|
|
1842
|
+
event_count: number;
|
|
1843
|
+
properties: Record<string, unknown>;
|
|
1844
|
+
recent_events: PalbaseEventRow[];
|
|
1845
|
+
}
|
|
1846
|
+
/** Create link params. */
|
|
1847
|
+
interface PalbaseCreateLinkParams {
|
|
1848
|
+
url: string;
|
|
1849
|
+
title?: string;
|
|
1850
|
+
description?: string;
|
|
1851
|
+
imageUrl?: string;
|
|
1852
|
+
ios?: {
|
|
1853
|
+
bundleId: string;
|
|
1854
|
+
appStoreId?: string;
|
|
1855
|
+
fallbackUrl?: string;
|
|
1856
|
+
minimumVersion?: string;
|
|
1857
|
+
};
|
|
1858
|
+
android?: {
|
|
1859
|
+
packageName: string;
|
|
1860
|
+
fallbackUrl?: string;
|
|
1861
|
+
minimumVersion?: number;
|
|
1862
|
+
};
|
|
1863
|
+
web?: {
|
|
1864
|
+
fallbackUrl?: string;
|
|
1865
|
+
};
|
|
1866
|
+
social?: {
|
|
1867
|
+
title?: string;
|
|
1868
|
+
description?: string;
|
|
1869
|
+
imageUrl?: string;
|
|
1870
|
+
};
|
|
1871
|
+
expiresAt?: string;
|
|
1872
|
+
customShortCode?: string;
|
|
1873
|
+
}
|
|
1874
|
+
/** Update link params. */
|
|
1875
|
+
interface PalbaseUpdateLinkParams {
|
|
1876
|
+
url?: string;
|
|
1877
|
+
title?: string;
|
|
1878
|
+
description?: string;
|
|
1879
|
+
imageUrl?: string;
|
|
1880
|
+
ios?: {
|
|
1881
|
+
bundleId?: string;
|
|
1882
|
+
appStoreId?: string;
|
|
1883
|
+
fallbackUrl?: string;
|
|
1884
|
+
minimumVersion?: string;
|
|
1885
|
+
};
|
|
1886
|
+
android?: {
|
|
1887
|
+
packageName?: string;
|
|
1888
|
+
fallbackUrl?: string;
|
|
1889
|
+
minimumVersion?: number;
|
|
1890
|
+
};
|
|
1891
|
+
web?: {
|
|
1892
|
+
fallbackUrl?: string;
|
|
1893
|
+
};
|
|
1894
|
+
social?: {
|
|
1895
|
+
title?: string;
|
|
1896
|
+
description?: string;
|
|
1897
|
+
imageUrl?: string;
|
|
1898
|
+
};
|
|
1899
|
+
expiresAt?: string;
|
|
1900
|
+
}
|
|
1901
|
+
/** A link object. */
|
|
1902
|
+
interface PalbaseLink {
|
|
1903
|
+
id: string;
|
|
1904
|
+
shortCode: string;
|
|
1905
|
+
shortUrl: string;
|
|
1906
|
+
url: string;
|
|
1907
|
+
title?: string;
|
|
1908
|
+
clickCount: number;
|
|
1909
|
+
createdAt: string;
|
|
1910
|
+
}
|
|
1911
|
+
/** Link details (extended link). */
|
|
1912
|
+
interface PalbaseLinkDetails extends PalbaseLink {
|
|
1913
|
+
description?: string;
|
|
1914
|
+
imageUrl?: string;
|
|
1915
|
+
ios?: Record<string, unknown>;
|
|
1916
|
+
android?: Record<string, unknown>;
|
|
1917
|
+
web?: Record<string, unknown>;
|
|
1918
|
+
social?: Record<string, unknown>;
|
|
1919
|
+
expiresAt?: string;
|
|
1920
|
+
}
|
|
1921
|
+
/** Link analytics. */
|
|
1922
|
+
interface PalbaseLinkAnalytics {
|
|
1923
|
+
totalClicks: number;
|
|
1924
|
+
clicksByPlatform: Record<string, number>;
|
|
1925
|
+
clicksByCountry: Record<string, number>;
|
|
1926
|
+
clicksByDay: Array<{
|
|
1927
|
+
date: string;
|
|
1928
|
+
clicks: number;
|
|
1929
|
+
}>;
|
|
1930
|
+
}
|
|
1931
|
+
/** QR code options. */
|
|
1932
|
+
interface PalbaseQrCodeOptions {
|
|
1933
|
+
size?: number;
|
|
1934
|
+
format?: "png" | "svg";
|
|
1935
|
+
}
|
|
1936
|
+
/** Match params for deferred deep link resolution. */
|
|
1937
|
+
interface PalbaseMatchParams {
|
|
1938
|
+
fingerprintHash: string;
|
|
1939
|
+
}
|
|
1940
|
+
/** Initial deep link. */
|
|
1941
|
+
interface PalbaseInitialLink {
|
|
1942
|
+
url: string;
|
|
1943
|
+
params?: Record<string, string>;
|
|
1944
|
+
}
|
|
1945
|
+
/** List links options. */
|
|
1946
|
+
interface PalbaseListLinksOptions {
|
|
1947
|
+
limit?: number;
|
|
1948
|
+
offset?: number;
|
|
1949
|
+
}
|
|
1950
|
+
/** List links result. */
|
|
1951
|
+
interface PalbaseListLinksResult {
|
|
1952
|
+
links: PalbaseLink[];
|
|
1953
|
+
total: number;
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Auth client surface available on `ctx.auth`.
|
|
1957
|
+
* Exposes server-relevant methods only. Browser-only patterns are omitted:
|
|
1958
|
+
* — signUp / signIn / signOut / refresh / requestPasswordReset / confirmPasswordReset
|
|
1959
|
+
* / changePassword / resendVerification / verifyEmail (client flow helpers)
|
|
1960
|
+
* — onAuthStateChange / onTokenChange (subscription callbacks)
|
|
1961
|
+
* — getOAuthURL / signInWithCredential (OAuth browser redirects)
|
|
1962
|
+
* — requestMagicLink / verifyMagicLink (client flow helpers)
|
|
1963
|
+
* — setTokens / getAccessToken (internal token management)
|
|
1964
|
+
* — listSessions / revokeSession / revokeAllSessions (user-self management)
|
|
1965
|
+
* — listIdentities / linkIdentity / unlinkIdentity (user-self management)
|
|
1966
|
+
* — listTrustedDevices / registerTrustedDevice / revokeTrustedDevice (user-self management)
|
|
1967
|
+
*/
|
|
1968
|
+
interface PalbaseAuthClient {
|
|
1969
|
+
/**
|
|
1970
|
+
* Verify a user's JWT by calling GET /auth/user. Returns the user if the
|
|
1971
|
+
* token is valid; error otherwise. Service-role privileged.
|
|
1972
|
+
*/
|
|
1973
|
+
verifyUserToken(jwt: string): Promise<PalbaseResult<PalbaseUser>>;
|
|
1974
|
+
/**
|
|
1975
|
+
* Get the current session held by the client (synchronous — no network
|
|
1976
|
+
* call). On the server the managed-runtime client does not hold a user
|
|
1977
|
+
* session; this always returns `{ data: null, error: null }`.
|
|
1978
|
+
*/
|
|
1979
|
+
getSession(): {
|
|
1980
|
+
data: PalbaseSession | null;
|
|
1981
|
+
error: null;
|
|
1982
|
+
};
|
|
1983
|
+
/** MFA admin surface — enroll/verify/manage factors on behalf of users. */
|
|
1984
|
+
mfa: {
|
|
1985
|
+
/** Enroll a new MFA factor (TOTP or email). */
|
|
1986
|
+
enroll(params: {
|
|
1987
|
+
type: "totp" | "email";
|
|
1988
|
+
}): Promise<PalbaseResult<PalbaseMFAEnrollResult>>;
|
|
1989
|
+
/** Verify an enrollment code. */
|
|
1990
|
+
verifyEnrollment(code: string): Promise<PalbaseResult<{
|
|
1991
|
+
status: string;
|
|
1992
|
+
}>>;
|
|
1993
|
+
/** Challenge an MFA factor (verify code, obtain token). */
|
|
1994
|
+
challenge(params: {
|
|
1995
|
+
mfa_token: string;
|
|
1996
|
+
type: "totp" | "email";
|
|
1997
|
+
code: string;
|
|
1998
|
+
}): Promise<PalbaseResult<PalbaseTokenResponse>>;
|
|
1999
|
+
/** Recover via backup code. */
|
|
2000
|
+
recovery(params: {
|
|
2001
|
+
mfa_token: string;
|
|
2002
|
+
code: string;
|
|
2003
|
+
}): Promise<PalbaseResult<PalbaseTokenResponse>>;
|
|
2004
|
+
/** List enrolled factors. */
|
|
2005
|
+
listFactors(): Promise<PalbaseResult<{
|
|
2006
|
+
factors: PalbaseMFAFactor[];
|
|
2007
|
+
}>>;
|
|
2008
|
+
/** Remove a factor. Requires current password. */
|
|
2009
|
+
removeFactor(factorId: string, currentPassword: string): Promise<PalbaseResult<{
|
|
2010
|
+
status: string;
|
|
2011
|
+
}>>;
|
|
2012
|
+
/** Regenerate recovery codes. */
|
|
2013
|
+
regenerateRecoveryCodes(): Promise<PalbaseResult<{
|
|
2014
|
+
recovery_codes: string[];
|
|
2015
|
+
}>>;
|
|
2016
|
+
/** Email MFA: start enrollment. */
|
|
2017
|
+
emailEnroll(): Promise<PalbaseResult<{
|
|
2018
|
+
status: string;
|
|
2019
|
+
}>>;
|
|
2020
|
+
/** Email MFA: send challenge. */
|
|
2021
|
+
emailChallenge(params: {
|
|
2022
|
+
mfa_token: string;
|
|
2023
|
+
}): Promise<PalbaseResult<{
|
|
2024
|
+
status: string;
|
|
2025
|
+
}>>;
|
|
2026
|
+
/** Email MFA: verify code. */
|
|
2027
|
+
emailVerify(params: {
|
|
2028
|
+
mfa_token: string;
|
|
2029
|
+
code: string;
|
|
2030
|
+
}): Promise<PalbaseResult<PalbaseTokenResponse>>;
|
|
2031
|
+
};
|
|
2032
|
+
/** Device attestation surface (App Attest / Play Integrity). */
|
|
2033
|
+
device: {
|
|
2034
|
+
/** Generate a device attestation challenge. */
|
|
2035
|
+
generateChallenge(): Promise<PalbaseResult<{
|
|
2036
|
+
challenge: string;
|
|
2037
|
+
}>>;
|
|
2038
|
+
/** Attest an Android device with a Play Integrity verdict token. */
|
|
2039
|
+
attestAndroid(params: PalbaseAttestAndroidParams): Promise<PalbaseResult<PalbaseAttestAndroidResult>>;
|
|
2040
|
+
/** Attest an iOS device with App Attest attestation data. */
|
|
2041
|
+
attestiOS(params: PalbaseAttestiOSParams): Promise<PalbaseResult<PalbaseAttestiOSResult>>;
|
|
2042
|
+
/** Bind a verified device with a public key for request signing. */
|
|
2043
|
+
bind(params: PalbaseBindDeviceParams): Promise<PalbaseResult<{
|
|
2044
|
+
success: boolean;
|
|
2045
|
+
}>>;
|
|
2046
|
+
/** List all devices for the current user. */
|
|
2047
|
+
list(): Promise<PalbaseResult<{
|
|
2048
|
+
devices: PalbaseDeviceInfo[];
|
|
2049
|
+
}>>;
|
|
2050
|
+
/** Delete a device by ID. */
|
|
2051
|
+
delete(deviceId: string): Promise<PalbaseResult<{
|
|
2052
|
+
success: boolean;
|
|
2053
|
+
}>>;
|
|
2054
|
+
/**
|
|
2055
|
+
* Verify a request signature from a device (server-only).
|
|
2056
|
+
* Not exposed in the client SDK.
|
|
2057
|
+
*/
|
|
2058
|
+
verifyRequestSignature(deviceId: string, params: PalbaseVerifyRequestSignatureParams): Promise<PalbaseResult<{
|
|
2059
|
+
verified: boolean;
|
|
2060
|
+
}>>;
|
|
2061
|
+
/** Get the cached App Check token, or null if not available / expired. */
|
|
2062
|
+
getToken(): string | null;
|
|
2063
|
+
/** Whether App Check is active (token cached and not expired). */
|
|
2064
|
+
readonly isActive: boolean;
|
|
2065
|
+
/** Set a cached App Check token manually (e.g. after attest flow). */
|
|
2066
|
+
setCachedToken(token: string, expiresInMs: number): void;
|
|
2067
|
+
/** Clean up timers and cached state. */
|
|
2068
|
+
dispose(): void;
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Bucket-level file operations available via `ctx.storage.bucket(name)`.
|
|
2073
|
+
* `getPublicUrl` is synchronous (no network call — constructs URL locally).
|
|
2074
|
+
*/
|
|
2075
|
+
interface PalbaseBucketClient {
|
|
2076
|
+
/** Upload a file. */
|
|
2077
|
+
upload(path: string, file: Blob | ArrayBuffer | ReadableStream, options?: PalbaseUploadOptions): Promise<PalbaseResult<PalbaseFileObject>>;
|
|
2078
|
+
/** Download a file as a Blob. */
|
|
2079
|
+
download(path: string): Promise<PalbaseResult<Blob>>;
|
|
2080
|
+
/**
|
|
2081
|
+
* The public URL for an object, or for one of its declared renditions.
|
|
2082
|
+
*
|
|
2083
|
+
* `variant` names a rendition from `config/storage.ts` — NOT arbitrary
|
|
2084
|
+
* dimensions. Renditions are produced at upload and served as immutable
|
|
2085
|
+
* objects, so a URL here costs nothing to serve and caches forever; an
|
|
2086
|
+
* arbitrary size would be a transform on the request path, one public URL
|
|
2087
|
+
* away from a CPU amplification attack.
|
|
2088
|
+
*
|
|
2089
|
+
* Asking for a variant the bucket does not declare is an error rather than a
|
|
2090
|
+
* silent fall back to the original: an app that requested a 200px thumbnail
|
|
2091
|
+
* and quietly received a 5 MB photograph works, looks right, and is found
|
|
2092
|
+
* months later on a bandwidth bill.
|
|
2093
|
+
*/
|
|
2094
|
+
getPublicUrl(path: string, options?: {
|
|
2095
|
+
variant?: string;
|
|
2096
|
+
}): string;
|
|
2097
|
+
/**
|
|
2098
|
+
* A time-limited URL for a private object.
|
|
2099
|
+
*
|
|
2100
|
+
* The duration is NAMED (`{ expiresIn: "1h" }`) rather than a bare number,
|
|
2101
|
+
* because "3600" reads as seconds to whoever wrote it and as minutes to
|
|
2102
|
+
* whoever maintains it. Same human spelling `fileSizeLimit` takes.
|
|
2103
|
+
*/
|
|
2104
|
+
createSignedUrl(path: string, options: {
|
|
2105
|
+
expiresIn: string;
|
|
2106
|
+
}): Promise<PalbaseResult<PalbaseSignedUrlResponse>>;
|
|
2107
|
+
/** List objects in the bucket (optionally filtered by prefix). */
|
|
2108
|
+
list(prefix?: string, options?: PalbaseListOptions): Promise<PalbaseResult<PalbaseFileObject[]>>;
|
|
2109
|
+
/** Delete one or more objects. */
|
|
2110
|
+
remove(paths: string[]): Promise<PalbaseResult<PalbaseFileObject[]>>;
|
|
2111
|
+
/** Move / rename an object. */
|
|
2112
|
+
move(from: string, to: string): Promise<PalbaseResult<void>>;
|
|
2113
|
+
/** Copy an object. */
|
|
2114
|
+
copy(from: string, to: string): Promise<PalbaseResult<void>>;
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Storage client available on `ctx.storage`.
|
|
2118
|
+
* Only `bucket()` is exposed — managed-runtime callers select a bucket first.
|
|
2119
|
+
*/
|
|
2120
|
+
interface PalbaseStorageClient {
|
|
2121
|
+
/** Get a bucket-scoped client for file operations. */
|
|
2122
|
+
bucket(name: string): PalbaseBucketClient;
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Functions client available on `ctx.functions`.
|
|
2126
|
+
* Invoke edge functions from a backend endpoint.
|
|
2127
|
+
*/
|
|
2128
|
+
interface PalbaseFunctionsClient {
|
|
2129
|
+
/** Invoke a named edge function. */
|
|
2130
|
+
invoke<T = unknown>(fnName: string, options?: PalbaseInvokeOptions): Promise<PalbaseResult<T>>;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Cross-user admin write surface, reached via `Flags.asService()`.
|
|
2134
|
+
*
|
|
2135
|
+
* These five methods set/clear overrides for an ARBITRARY user (named
|
|
2136
|
+
* explicitly), so they bypass the current-request-user scope that `Flags.*`
|
|
2137
|
+
* reads and `Flags.setOverride(...)` are bound to. Mirrors the
|
|
2138
|
+
* `Database` / `Database.$asService()` split: the privileged, cross-user path
|
|
2139
|
+
* is moved OFF the default surface so it is greppable and intentional — a
|
|
2140
|
+
* handler that calls `Flags.setOverrideForUser(...)` on the default surface is
|
|
2141
|
+
* a compile error and must reach for `Flags.asService()` first.
|
|
2142
|
+
*
|
|
2143
|
+
* @example
|
|
2144
|
+
* await Flags.asService().setOverrideForUser("user_123", "new_checkout", true);
|
|
2145
|
+
*/
|
|
2146
|
+
interface PalbaseFlagsServiceClient {
|
|
2147
|
+
/**
|
|
2148
|
+
* Set (or replace) a single feature-flag override for one user. The override
|
|
2149
|
+
* shadows the project (system) default for that user until cleared.
|
|
2150
|
+
*/
|
|
2151
|
+
setOverrideForUser(userId: string, key: PalbaseFlagKey, value: PalbaseFlagValue): Promise<PalbaseResult<PalbaseSetOverrideResult>>;
|
|
2152
|
+
/** Set multiple overrides for one user in a single call. */
|
|
2153
|
+
setOverridesForUser(userId: string, values: Record<string, PalbaseFlagValue>): Promise<PalbaseResult<PalbaseSetOverridesResult>>;
|
|
2154
|
+
/** Clear one override for a user, restoring the project (system) default. */
|
|
2155
|
+
clearOverrideForUser(userId: string, key: PalbaseFlagKey): Promise<PalbaseResult<PalbaseClearOverrideResult>>;
|
|
2156
|
+
/** Clear all overrides for a user, restoring project (system) defaults. */
|
|
2157
|
+
clearAllOverridesForUser(userId: string): Promise<PalbaseResult<PalbaseClearAllOverridesResult>>;
|
|
2158
|
+
/** Apply override writes across many users in one request (max 1000 ops). */
|
|
2159
|
+
batchSetOverrides(operations: ReadonlyArray<PalbaseBatchOverrideOperation>): Promise<PalbaseResult<PalbaseBatchSetOverridesResult>>;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Flags client available as the `Flags` singleton (and on `ctx.flags`).
|
|
2163
|
+
* Evaluate feature flags server-side (managed-runtime key — user targeting
|
|
2164
|
+
* is optional via context).
|
|
2165
|
+
*
|
|
2166
|
+
* Writes follow the `Database` model:
|
|
2167
|
+
* — `setOverride(key, value)` (default) overrides the flag for the CURRENT
|
|
2168
|
+
* request user; no userId argument, no admin power required. It errors when
|
|
2169
|
+
* there is no signed-in user (an anonymous request).
|
|
2170
|
+
* — `asService()` returns the {@link PalbaseFlagsServiceClient} carrying the
|
|
2171
|
+
* cross-user admin writes (`setOverrideForUser`, …) for an arbitrary target
|
|
2172
|
+
* user. Greppable + explicit, exactly like `Database.$asService()`.
|
|
2173
|
+
*
|
|
2174
|
+
* Reads (`isEnabled`/`getVariant`/`get`/`getAll`) stay on this default surface
|
|
2175
|
+
* and are already current-user-scoped via the request identity.
|
|
2176
|
+
*/
|
|
2177
|
+
interface PalbaseFlagsClient {
|
|
2178
|
+
/** Is a flag enabled for the given context? */
|
|
2179
|
+
isEnabled(flagName: PalbaseFlagKey, context?: PalbaseFlagContext): Promise<PalbaseResult<boolean>>;
|
|
2180
|
+
/** Get the active variant of a multivariate flag. */
|
|
2181
|
+
getVariant(flagName: PalbaseFlagKey, context?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlagVariant>>;
|
|
2182
|
+
/** Get all flags for the project. */
|
|
2183
|
+
getAll(context?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlag[]>>;
|
|
2184
|
+
/**
|
|
2185
|
+
* Resolve a flag's value, with an optional fallback.
|
|
2186
|
+
*
|
|
2187
|
+
* `get(key)` · `get(key, default)` · `get(key, ctx)` · `get(key, default, ctx)`.
|
|
2188
|
+
* The default is substituted when the flag is absent AND when the flags
|
|
2189
|
+
* service is unreachable — a product that hides a feature because a lookup
|
|
2190
|
+
* timed out is behaving correctly.
|
|
2191
|
+
*
|
|
2192
|
+
* Declared here as of 2026-08-15. The client has always had it and its tests
|
|
2193
|
+
* have always covered it, but the interface did not: `Flags.get("x")` was a
|
|
2194
|
+
* type error in a handler and worked at runtime. That is the same gap as
|
|
2195
|
+
* `Documents.doc(...)` — declared and never implemented — seen from the other
|
|
2196
|
+
* side, and moving the implementation into this package is what surfaced it.
|
|
2197
|
+
*/
|
|
2198
|
+
get(flagName: PalbaseFlagKey, defaultOrContext?: PalbaseFlagValue | PalbaseFlagContext, maybeContext?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlagValue>>;
|
|
2199
|
+
/**
|
|
2200
|
+
* Set (or replace) a single feature-flag override for the CURRENT REQUEST
|
|
2201
|
+
* USER. No userId argument — the override is bound to the signed-in user the
|
|
2202
|
+
* handler is serving, so a handler can flip a flag for that user without
|
|
2203
|
+
* admin power. The override shadows the project (system) default for that
|
|
2204
|
+
* user until cleared.
|
|
2205
|
+
*
|
|
2206
|
+
* Errors when there is no signed-in user (an anonymous request); reach for
|
|
2207
|
+
* `Flags.asService().setOverrideForUser(userId, key, value)` to write a flag
|
|
2208
|
+
* for an arbitrary (cross-user) target.
|
|
2209
|
+
*
|
|
2210
|
+
* @example
|
|
2211
|
+
* await Flags.setOverride("new_checkout", true);
|
|
2212
|
+
*/
|
|
2213
|
+
setOverride(key: PalbaseFlagKey, value: PalbaseFlagValue): Promise<PalbaseResult<PalbaseSetOverrideResult>>;
|
|
2214
|
+
/**
|
|
2215
|
+
* Return the cross-user admin write surface ({@link PalbaseFlagsServiceClient}).
|
|
2216
|
+
* Use sparingly and explicitly — the default `Flags.setOverride(...)` path is
|
|
2217
|
+
* bound to the current request user; `asService()` is how you write a flag
|
|
2218
|
+
* override for an ARBITRARY user. Mirrors `Database.$asService()`.
|
|
2219
|
+
*/
|
|
2220
|
+
asService(): PalbaseFlagsServiceClient;
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Realtime broadcast surface (backend handler → subscribed clients).
|
|
2224
|
+
*
|
|
2225
|
+
* **Every channel must be DECLARED, or the join is refused.** Channel names are
|
|
2226
|
+
* not free text: `channels.ts` at the project root publishes which names exist
|
|
2227
|
+
* and who may subscribe, publish, or write state, and a client joining a name no
|
|
2228
|
+
* declaration matches is rejected without ever reaching your code. Declare with
|
|
2229
|
+
* `ownerOnly()`, `publicChannel()`, or a custom `authorize(ctx)` — see
|
|
2230
|
+
* {@link defineChannels}. Broadcasting to an undeclared channel reaches nobody.
|
|
2231
|
+
*
|
|
2232
|
+
* Backend-side this is BROADCAST-ONLY: a handler pushes an event to a channel
|
|
2233
|
+
* and every client subscribed to that channel (via the client SDK's
|
|
2234
|
+
* `pb.realtime.channel(...).on(...)`) receives it. There is no `subscribe()` on
|
|
2235
|
+
* the backend — a stateless request handler can't hold a socket; it fires an
|
|
2236
|
+
* HTTP broadcast and returns. Delivery is fire-and-forget: `broadcast` resolves
|
|
2237
|
+
* once the broadcast is accepted (or with an `error` if it could not be sent),
|
|
2238
|
+
* but it never blocks the handler waiting on subscribers.
|
|
2239
|
+
*
|
|
2240
|
+
* Channel names are yours to choose (e.g. `"room:42"`, `"orders"`) but not yours
|
|
2241
|
+
* to leave undeclared: each must match a pattern in `channels.ts`, which is where
|
|
2242
|
+
* `"room:{roomId}"` is authorized. The Palbase-managed `flags:<ref>` channels are
|
|
2243
|
+
* internal — author channels for your own features.
|
|
2244
|
+
*
|
|
2245
|
+
* @example
|
|
2246
|
+
* // Notify everyone in a chat room that a message landed:
|
|
2247
|
+
* await Realtime.broadcast("room:42", "message", { text, from: user.id });
|
|
2248
|
+
*/
|
|
2249
|
+
interface PalbaseRealtimeClient {
|
|
2250
|
+
/**
|
|
2251
|
+
* A channel's SHARED STATE — a keyed map every subscriber sees, including
|
|
2252
|
+
* the ones that join later.
|
|
2253
|
+
*
|
|
2254
|
+
* That is the whole difference from `broadcast`: a broadcast reaches whoever
|
|
2255
|
+
* is listening at that instant, and a client connecting a second afterwards
|
|
2256
|
+
* learns nothing until the next one. State is handed to every joiner in the
|
|
2257
|
+
* snapshot they get, so "the score is 3-1" or "this flag is now on" arrives
|
|
2258
|
+
* even for a client that was not there when it changed.
|
|
2259
|
+
*/
|
|
2260
|
+
state: {
|
|
2261
|
+
/** Write one entry. Fire-and-forget, like `broadcast`. */
|
|
2262
|
+
set(topic: string, key: string, value: Record<string, unknown>): Promise<PalbaseResult<void>>;
|
|
2263
|
+
/** Remove one entry. */
|
|
2264
|
+
clear(topic: string, key: string): Promise<PalbaseResult<void>>;
|
|
2265
|
+
};
|
|
2266
|
+
/**
|
|
2267
|
+
* Broadcast `event` with `payload` to everyone subscribed to `channel`.
|
|
2268
|
+
*
|
|
2269
|
+
* Fire-and-forget: resolves `{ data: undefined, error: null }` when the
|
|
2270
|
+
* broadcast was accepted, or `{ data: null, error }` when it could not be
|
|
2271
|
+
* sent (e.g. realtime not provisioned). A failed broadcast never throws and
|
|
2272
|
+
* never fails the handler.
|
|
2273
|
+
*
|
|
2274
|
+
* @param channel App-defined channel name (e.g. `"room:42"`). Do NOT prefix
|
|
2275
|
+
* with `"realtime:"` — that prefix is internal to the transport.
|
|
2276
|
+
* @param event Event name subscribers filter on (e.g. `"message"`).
|
|
2277
|
+
* @param payload JSON-serializable event body.
|
|
2278
|
+
*/
|
|
2279
|
+
broadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<PalbaseResult<void>>;
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Push sub-client surface (server-only: fan-out to users / topics).
|
|
2283
|
+
*/
|
|
2284
|
+
interface PalbasePushClient {
|
|
2285
|
+
/** Send a push notification to one or more users, or a topic. */
|
|
2286
|
+
send(params: PalbasePushSendParams): Promise<PalbaseResult<PalbasePushSendResponse | PalbaseMultiChannelResponse>>;
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Email sub-client surface (managed-runtime).
|
|
2290
|
+
*/
|
|
2291
|
+
interface PalbaseEmailClient {
|
|
2292
|
+
/** Send a transactional email. */
|
|
2293
|
+
send(params: PalbaseEmailSendParams): Promise<PalbaseResult<PalbaseEmailSendResponse>>;
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* SMS sub-client surface (managed-runtime).
|
|
2297
|
+
*/
|
|
2298
|
+
interface PalbaseSmsClient {
|
|
2299
|
+
/** Send an SMS message. */
|
|
2300
|
+
send(params: PalbaseSmsSendParams): Promise<PalbaseResult<PalbaseSmsSendResponse>>;
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Phone-verification (OTP) sub-client surface (managed-runtime).
|
|
2304
|
+
*
|
|
2305
|
+
* Separate from `sms` because a verification carries no caller-authored text.
|
|
2306
|
+
* It is also the rail that works without a registered sender: a verification
|
|
2307
|
+
* service owns its own sender identity, which plain SMS requires you to
|
|
2308
|
+
* provision (and, in many countries, register) yourself.
|
|
2309
|
+
*/
|
|
2310
|
+
interface PalbaseVerificationsClient {
|
|
2311
|
+
/** Send a verification code over SMS. */
|
|
2312
|
+
start(params: PalbaseVerificationStartParams): Promise<PalbaseResult<PalbaseVerificationStartResponse>>;
|
|
2313
|
+
/** Check a submitted code. A wrong code resolves to `approved: false`. */
|
|
2314
|
+
check(params: PalbaseVerificationCheckParams): Promise<PalbaseResult<PalbaseVerificationCheckResponse>>;
|
|
2315
|
+
}
|
|
2316
|
+
/**
|
|
2317
|
+
* Inbox sub-client surface (managed-runtime send + user read operations).
|
|
2318
|
+
*/
|
|
2319
|
+
interface PalbaseInboxClient {
|
|
2320
|
+
/** Service-role: create an inbox notification row for a user. */
|
|
2321
|
+
send(params: PalbaseInboxSendParams): Promise<PalbaseResult<PalbaseInboxSendResponse | PalbaseMultiChannelResponse>>;
|
|
2322
|
+
/** List inbox messages (user-scoped or admin). */
|
|
2323
|
+
list(options?: PalbaseInboxListOptions): Promise<PalbaseResult<PalbaseInboxListResult>>;
|
|
2324
|
+
/** Count unread messages. */
|
|
2325
|
+
unreadCount(): Promise<PalbaseResult<{
|
|
2326
|
+
count: number;
|
|
2327
|
+
}>>;
|
|
2328
|
+
/** Mark a message as read. */
|
|
2329
|
+
markRead(id: string): Promise<PalbaseResult<void>>;
|
|
2330
|
+
/** Mark all messages read. */
|
|
2331
|
+
markAllRead(): Promise<PalbaseResult<void>>;
|
|
2332
|
+
/** Archive (soft-delete) a message. */
|
|
2333
|
+
archive(id: string): Promise<PalbaseResult<void>>;
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* Preferences sub-client surface.
|
|
2337
|
+
*/
|
|
2338
|
+
interface PalbasePreferencesClient {
|
|
2339
|
+
/** Get notification preferences. */
|
|
2340
|
+
get(): Promise<PalbaseResult<PalbasePreferences>>;
|
|
2341
|
+
/** Update notification preferences. */
|
|
2342
|
+
update(params: PalbasePreferences): Promise<PalbaseResult<PalbasePreferences>>;
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Email-template CRUD sub-client (managed-runtime gated server-side).
|
|
2346
|
+
*/
|
|
2347
|
+
interface PalbaseEmailTemplatesClient {
|
|
2348
|
+
/** List all email templates. */
|
|
2349
|
+
list(): Promise<PalbaseResult<PalbaseEmailTemplate[]>>;
|
|
2350
|
+
/** Get one email template by ID. */
|
|
2351
|
+
get(id: string): Promise<PalbaseResult<PalbaseEmailTemplate>>;
|
|
2352
|
+
/** Create an email template. */
|
|
2353
|
+
create(input: PalbaseCreateEmailTemplateInput): Promise<PalbaseResult<PalbaseEmailTemplate>>;
|
|
2354
|
+
/** Update an email template (patch semantics). */
|
|
2355
|
+
update(id: string, input: PalbaseUpdateEmailTemplateInput): Promise<PalbaseResult<PalbaseEmailTemplate>>;
|
|
2356
|
+
/** Delete an email template. */
|
|
2357
|
+
delete(id: string): Promise<PalbaseResult<void>>;
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* SMS-template CRUD sub-client (managed-runtime gated server-side).
|
|
2361
|
+
*/
|
|
2362
|
+
interface PalbaseSMSTemplatesClient {
|
|
2363
|
+
/** List all SMS templates. */
|
|
2364
|
+
list(): Promise<PalbaseResult<PalbaseSMSTemplate[]>>;
|
|
2365
|
+
/** Get one SMS template by ID. */
|
|
2366
|
+
get(id: string): Promise<PalbaseResult<PalbaseSMSTemplate>>;
|
|
2367
|
+
/** Create an SMS template. */
|
|
2368
|
+
create(input: PalbaseCreateSMSTemplateInput): Promise<PalbaseResult<PalbaseSMSTemplate>>;
|
|
2369
|
+
/** Update an SMS template (patch semantics). */
|
|
2370
|
+
update(id: string, input: PalbaseUpdateSMSTemplateInput): Promise<PalbaseResult<PalbaseSMSTemplate>>;
|
|
2371
|
+
/** Delete an SMS template. */
|
|
2372
|
+
delete(id: string): Promise<PalbaseResult<void>>;
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Templates sub-client — parallel CRUD for email and SMS templates.
|
|
2376
|
+
* Service-role gated server-side; both surfaces map TS camelCase to
|
|
2377
|
+
* wire snake_case in the runtime client.
|
|
2378
|
+
*/
|
|
2379
|
+
interface PalbaseTemplatesClient {
|
|
2380
|
+
/** Email template CRUD. */
|
|
2381
|
+
email: PalbaseEmailTemplatesClient;
|
|
2382
|
+
/** SMS template CRUD. */
|
|
2383
|
+
sms: PalbaseSMSTemplatesClient;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Notifications client available on `ctx.notifications`.
|
|
2387
|
+
* Service-role: all send operations require privileged access.
|
|
2388
|
+
* Omits ClientNotificationsClient (browser-only narrowed surface).
|
|
2389
|
+
*/
|
|
2390
|
+
interface PalbaseNotificationsClient {
|
|
2391
|
+
/** Push notification sender. */
|
|
2392
|
+
push: PalbasePushClient;
|
|
2393
|
+
/** Email sender. */
|
|
2394
|
+
email: PalbaseEmailClient;
|
|
2395
|
+
/** SMS sender. */
|
|
2396
|
+
sms: PalbaseSmsClient;
|
|
2397
|
+
/** Phone verification (OTP) sender and checker. */
|
|
2398
|
+
verifications: PalbaseVerificationsClient;
|
|
2399
|
+
/** Inbox (in-app) message sender and reader. */
|
|
2400
|
+
inbox: PalbaseInboxClient;
|
|
2401
|
+
/** Notification preferences manager. */
|
|
2402
|
+
preferences: PalbasePreferencesClient;
|
|
2403
|
+
/** Email + SMS template CRUD (managed-runtime). */
|
|
2404
|
+
templates: PalbaseTemplatesClient;
|
|
2405
|
+
/** Register a device for push notifications. */
|
|
2406
|
+
registerDevice(params: PalbaseRegisterDeviceParams): Promise<PalbaseResult<PalbaseDeviceTokenView>>;
|
|
2407
|
+
/** Remove a device registration. */
|
|
2408
|
+
unregisterDevice(deviceId: string): Promise<PalbaseResult<void>>;
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Analytics query namespace (read-side endpoints).
|
|
2412
|
+
*/
|
|
2413
|
+
interface PalbaseAnalyticsQueryNamespace {
|
|
2414
|
+
/** Count events over time. */
|
|
2415
|
+
count(input: PalbaseCountQueryInput): Promise<PalbaseResult<PalbaseCountResult>>;
|
|
2416
|
+
/** List raw events. */
|
|
2417
|
+
events(input: PalbaseEventsQueryInput): Promise<PalbaseResult<PalbaseEventsResult>>;
|
|
2418
|
+
/** List event property descriptors. */
|
|
2419
|
+
properties(input?: PalbasePropertiesQueryInput): Promise<PalbaseResult<PalbasePropertyDescriptor[]>>;
|
|
2420
|
+
/** Query users by filters. */
|
|
2421
|
+
users(input: PalbaseUsersQueryInput): Promise<PalbaseResult<PalbaseUsersResult>>;
|
|
2422
|
+
/** Run a funnel query. */
|
|
2423
|
+
funnel(input: PalbaseFunnelQueryInput): Promise<PalbaseResult<PalbaseFunnelResult>>;
|
|
2424
|
+
/** Run a retention query. */
|
|
2425
|
+
retention(input: PalbaseRetentionQueryInput): Promise<PalbaseResult<PalbaseRetentionResult>>;
|
|
2426
|
+
/** Run a cohort query. */
|
|
2427
|
+
cohort(input: PalbaseCohortQueryInput): Promise<PalbaseResult<PalbaseCohortResult>>;
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Analytics management namespace (meta-level endpoints).
|
|
2431
|
+
*/
|
|
2432
|
+
interface PalbaseAnalyticsManagementNamespace {
|
|
2433
|
+
/** Get project-level overview stats. */
|
|
2434
|
+
overview(): Promise<PalbaseResult<PalbaseOverviewResult>>;
|
|
2435
|
+
/** Get all event names seen by the project. */
|
|
2436
|
+
eventNames(): Promise<PalbaseResult<PalbaseEventNamesResult>>;
|
|
2437
|
+
/** Get details for a specific user. */
|
|
2438
|
+
userDetail(distinctId: string): Promise<PalbaseResult<PalbaseUserDetailResult>>;
|
|
2439
|
+
/** Delete all data for a user (GDPR erasure). */
|
|
2440
|
+
deleteUser(distinctId: string): Promise<PalbaseResult<void>>;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Analytics client available on `ctx.analytics`.
|
|
2444
|
+
* Capture events server-side and run analytical queries.
|
|
2445
|
+
*/
|
|
2446
|
+
interface PalbaseAnalyticsClient {
|
|
2447
|
+
/** Capture a custom event. */
|
|
2448
|
+
capture(event: string, properties?: PalbaseAnalyticsProperties, distinctId?: string): Promise<PalbaseResult<void>>;
|
|
2449
|
+
/** Identify a user with traits. */
|
|
2450
|
+
identify(distinctId: string, traits?: PalbaseIdentifyTraits): Promise<PalbaseResult<void>>;
|
|
2451
|
+
/** Track a screen view. */
|
|
2452
|
+
screen(screenName: string, properties?: PalbaseAnalyticsProperties, distinctId?: string): Promise<PalbaseResult<void>>;
|
|
2453
|
+
/** Query namespace for analytics read operations. */
|
|
2454
|
+
query: PalbaseAnalyticsQueryNamespace;
|
|
2455
|
+
/** Management namespace for meta-level operations. */
|
|
2456
|
+
management: PalbaseAnalyticsManagementNamespace;
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Deep-links client available on `ctx.links`.
|
|
2460
|
+
* Create, manage, and resolve deep links from server endpoints.
|
|
2461
|
+
* Omits `getInitialLink()` (client SDK convenience — browser/app only).
|
|
2462
|
+
*/
|
|
2463
|
+
interface PalbaseLinksClient {
|
|
2464
|
+
/** Create a short link. */
|
|
2465
|
+
create(params: PalbaseCreateLinkParams): Promise<PalbaseResult<PalbaseLink>>;
|
|
2466
|
+
/** List links (paginated). */
|
|
2467
|
+
list(options?: PalbaseListLinksOptions): Promise<PalbaseResult<PalbaseListLinksResult>>;
|
|
2468
|
+
/** Get link details + analytics summary. */
|
|
2469
|
+
get(linkId: string): Promise<PalbaseResult<PalbaseLinkDetails>>;
|
|
2470
|
+
/** Update a link. */
|
|
2471
|
+
update(linkId: string, params: PalbaseUpdateLinkParams): Promise<PalbaseResult<PalbaseLink>>;
|
|
2472
|
+
/** Delete a link. */
|
|
2473
|
+
delete(linkId: string): Promise<PalbaseResult<{
|
|
2474
|
+
success: boolean;
|
|
2475
|
+
}>>;
|
|
2476
|
+
/** Get click analytics for a link. */
|
|
2477
|
+
analytics(linkId: string): Promise<PalbaseResult<PalbaseLinkAnalytics>>;
|
|
2478
|
+
/** Generate a QR code for a link (returns PNG or SVG Blob). */
|
|
2479
|
+
qrCode(linkId: string, options?: PalbaseQrCodeOptions): Promise<PalbaseResult<Blob>>;
|
|
2480
|
+
/** Match a deferred deep link by device fingerprint. */
|
|
2481
|
+
match(params: PalbaseMatchParams): Promise<PalbaseResult<PalbaseInitialLink | null>>;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
/**
|
|
2485
|
+
* runtime.ts — request-scoped service singletons.
|
|
2486
|
+
*
|
|
2487
|
+
* The backend SDK no longer threads a `ctx` god-object through every handler.
|
|
2488
|
+
* Instead, controller methods import PascalCase service singletons directly:
|
|
2489
|
+
*
|
|
2490
|
+
* import { Controller, Post, Body, Database } from "@palbase/backend";
|
|
2491
|
+
*
|
|
2492
|
+
* \@Controller("/todos")
|
|
2493
|
+
* export default class TodosController {
|
|
2494
|
+
* \@Post("") create(\@Body(CreateTodoBody) body: CreateTodoBody): unknown {
|
|
2495
|
+
* return Database.insert("todos", { title: body.title });
|
|
2496
|
+
* }
|
|
2497
|
+
* }
|
|
2498
|
+
*
|
|
2499
|
+
* The singletons are thin Proxies. Every property access forwards to the live
|
|
2500
|
+
* client for the CURRENT request scope, resolved through {@link __getRuntime}.
|
|
2501
|
+
*
|
|
2502
|
+
* # Request-scope resolution (persistent app-server)
|
|
2503
|
+
*
|
|
2504
|
+
* The runtime is a long-running Node process that serves many concurrent
|
|
2505
|
+
* requests on one event loop (NOT a fresh subprocess per request). A single
|
|
2506
|
+
* module-global slot would let one in-flight request's services bleed into
|
|
2507
|
+
* another's. So the services are carried in an {@link AsyncLocalStorage} store
|
|
2508
|
+
* ({@link __requestALS}) that the runtime sets per request with
|
|
2509
|
+
* {@link __runWithRuntime}; every async continuation of that request reads its
|
|
2510
|
+
* own store. `__getRuntime` reads the ALS store first; the module-global slot
|
|
2511
|
+
* (set by {@link __setRuntime}) is only a fallback for callers that run OUTSIDE
|
|
2512
|
+
* an ALS scope (dev-server, unit tests, the legacy single-shot path). Because
|
|
2513
|
+
* each `br-<ref>` pod is single-tenant, there is no cross-tenant leakage; the
|
|
2514
|
+
* ALS store is what prevents cross-REQUEST leakage within the shared process.
|
|
2515
|
+
*
|
|
2516
|
+
* The seam that makes `import { Database } from "@palbase/backend"` resolve to
|
|
2517
|
+
* the runtime-injected client: `@palbase/backend` is marked esbuild-EXTERNAL
|
|
2518
|
+
* when the tenant bundle is built, and the package is installed globally in the
|
|
2519
|
+
* pod (NODE_PATH=/usr/local/lib/node_modules). So worker.js's
|
|
2520
|
+
* `require('@palbase/backend')` and the bundle's `import` resolve to ONE shared
|
|
2521
|
+
* module instance — the ALS store and `__setRuntime` slot on that instance are
|
|
2522
|
+
* visible to the singletons the bundle imported.
|
|
2523
|
+
*/
|
|
2524
|
+
|
|
2525
|
+
/** The set of live clients the runtime injects per request scope.
|
|
2526
|
+
*
|
|
2527
|
+
* Realtime is BROADCAST-ONLY here (a stateless handler can push an event but
|
|
2528
|
+
* cannot hold a subscription socket — `subscribe()` lives on the client SDK).
|
|
2529
|
+
*
|
|
2530
|
+
* EXCLUDED on purpose: Functions, Links, Analytics, Auth. They are not
|
|
2531
|
+
* exposed as backend handler singletons (auth lives on the client SDK; the rest
|
|
2532
|
+
* are out of scope for backend endpoints). */
|
|
2533
|
+
interface RuntimeServices {
|
|
2534
|
+
Database: DBClient;
|
|
2535
|
+
Secrets: SecretsService;
|
|
2536
|
+
Documents: PalbaseDocsClient;
|
|
2537
|
+
Storage: PalbaseStorageClient;
|
|
2538
|
+
Cache: CacheClient;
|
|
2539
|
+
Log: Logger;
|
|
2540
|
+
Notifications: PalbaseNotificationsClient;
|
|
2541
|
+
Flags: PalbaseFlagsClient;
|
|
2542
|
+
Realtime: PalbaseRealtimeClient;
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* The per-request ALS box.
|
|
2546
|
+
*
|
|
2547
|
+
* `runtime` is the service bundle. `userId` is the request's authenticated user
|
|
2548
|
+
* id, written by the runtime immediately after it assembles the request object
|
|
2549
|
+
* (worker.js: `requestALS.getStore().userId = pbReq.user?.id || null`) and
|
|
2550
|
+
* `null` on an anonymous request. It was already being written there for the
|
|
2551
|
+
* Flags client's auto-bind; declaring it here makes the existing contract typed
|
|
2552
|
+
* instead of implicit, which is what lets `currentSubjectId()` read a
|
|
2553
|
+
* server-owned identity rather than trusting anything the caller sent.
|
|
2554
|
+
*/
|
|
2555
|
+
interface RequestStore {
|
|
2556
|
+
runtime: RuntimeServices;
|
|
2557
|
+
userId?: string | null;
|
|
2558
|
+
/** This request's id (`req_…`), written by the runtime. Used as the spend
|
|
2559
|
+
* idempotency scope when the caller supplied no `Idempotency-Key`. */
|
|
2560
|
+
requestId?: string;
|
|
2561
|
+
/** The caller's `Idempotency-Key` header, or `null` when absent. What makes a
|
|
2562
|
+
* client's retry replay its first result instead of charging twice. */
|
|
2563
|
+
idempotencyKey?: string | null;
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* Per-request store. The persistent runtime runs each request inside
|
|
2567
|
+
* {@link __runWithRuntime}, so every async continuation of that request reads
|
|
2568
|
+
* its OWN `runtime` (and any other request-scoped fields the runtime adds).
|
|
2569
|
+
*
|
|
2570
|
+
* Exported with a `__` prefix so the runtime (worker.js) shares the SAME ALS
|
|
2571
|
+
* instance across the one module instance — two ALS instances would silently
|
|
2572
|
+
* not see each other's stores. NOT part of the public author-facing API.
|
|
2573
|
+
*/
|
|
2574
|
+
declare const __requestALS: AsyncLocalStorage<RequestStore>;
|
|
2575
|
+
/** Install the live clients in the process-global fallback slot.
|
|
2576
|
+
*
|
|
2577
|
+
* Persistent-server requests should use {@link __runWithRuntime} instead; this
|
|
2578
|
+
* remains for dev-server / tests / the legacy single-shot path that run without
|
|
2579
|
+
* an ALS scope. NOT part of the public author-facing API. */
|
|
2580
|
+
declare function __setRuntime(services: RuntimeServices): void;
|
|
2581
|
+
/** Run `fn` with `services` bound as the request-scoped runtime.
|
|
2582
|
+
*
|
|
2583
|
+
* The persistent worker calls this once per request so concurrent requests
|
|
2584
|
+
* never share a services slot. NOT part of the public author-facing API. */
|
|
2585
|
+
declare function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T;
|
|
2586
|
+
/** Read the live clients, throwing if accessed outside a request scope.
|
|
2587
|
+
*
|
|
2588
|
+
* Resolves the ALS store first (persistent server, per-request), then the
|
|
2589
|
+
* process-global fallback (dev-server / tests). NOT part of the public
|
|
2590
|
+
* author-facing API — used by the runtime and the singleton Proxies. */
|
|
2591
|
+
declare function __getRuntime(): RuntimeServices;
|
|
2592
|
+
/** A lifecycle hook. Sync or async; the runtime awaits what it returns. */
|
|
2593
|
+
type LifecycleHook = () => void | Promise<void>;
|
|
2594
|
+
/** Runs one release's shutdown hooks. Handed back by {@link __runStartHooks}
|
|
2595
|
+
* and called by the engine's `app.shutdown()`. Idempotent. */
|
|
2596
|
+
type ShutdownRunner = () => Promise<void>;
|
|
2597
|
+
/**
|
|
2598
|
+
* Run `hook` ONCE while the application comes up, before it serves anything.
|
|
2599
|
+
*
|
|
2600
|
+
* Call it at MODULE SCOPE in a file the application imports — the same rule
|
|
2601
|
+
* `defineDefaultAuth` and `@Controller` follow, and for the same reason: the
|
|
2602
|
+
* declaration is claimed when the app boots, which is after module loading and
|
|
2603
|
+
* before the first request. `name` is not decoration: a hook that throws is
|
|
2604
|
+
* reported by that name and the boot is REFUSED, so it is what tells an
|
|
2605
|
+
* operator which resource did not come up.
|
|
2606
|
+
*
|
|
2607
|
+
* There is no request scope yet, so the `Database`/`Secrets`/… singletons are
|
|
2608
|
+
* NOT available inside a start hook. A secret is read from `process.env` here
|
|
2609
|
+
* (the runtime mirrors the vault into it at boot).
|
|
2610
|
+
*
|
|
2611
|
+
* @example
|
|
2612
|
+
* // resources/graph.ts
|
|
2613
|
+
* import neo4j from "neo4j-driver";
|
|
2614
|
+
* import { onStart, onShutdown } from "@palbase/backend";
|
|
2615
|
+
*
|
|
2616
|
+
* export let graph: Driver;
|
|
2617
|
+
* onStart("graph", () => {
|
|
2618
|
+
* graph = neo4j.driver(process.env.NEO4J_URL!, neo4j.auth.basic("neo4j", process.env.NEO4J_PASSWORD!));
|
|
2619
|
+
* });
|
|
2620
|
+
* onShutdown("graph", () => graph.close());
|
|
2621
|
+
*/
|
|
2622
|
+
declare function onStart(name: string, hook: LifecycleHook): void;
|
|
2623
|
+
/**
|
|
2624
|
+
* Run `hook` while the application shuts down — the place a pool opened in
|
|
2625
|
+
* {@link onStart} is closed.
|
|
2626
|
+
*
|
|
2627
|
+
* Shutdown is BEST-EFFORT by design: a hook that throws is reported by name and
|
|
2628
|
+
* the rest still run. A drain that abandoned the remaining hooks on the first
|
|
2629
|
+
* failure would leak exactly what this exists to release, and the process is
|
|
2630
|
+
* leaving anyway.
|
|
2631
|
+
*
|
|
2632
|
+
* Hooks run in REVERSE declaration order, so a resource is released before what
|
|
2633
|
+
* it was built on.
|
|
2634
|
+
*/
|
|
2635
|
+
declare function onShutdown(name: string, hook: LifecycleHook): void;
|
|
2636
|
+
/**
|
|
2637
|
+
* CLAIM what has been declared, run the start hooks, and hand back the runner
|
|
2638
|
+
* for this release's shutdown hooks. Called by the engine's `createApp`; the
|
|
2639
|
+
* `App.shutdown()` it builds calls what comes back. NOT part of the public
|
|
2640
|
+
* author-facing API.
|
|
2641
|
+
*
|
|
2642
|
+
* IT CLAIMS RATHER THAN READS, which is what makes it correct in this runtime:
|
|
2643
|
+
* a candidate release is loaded BESIDE the live one in one process
|
|
2644
|
+
* (`v2/runtime/src/registry-scope.ts`), and both bundles append to the one
|
|
2645
|
+
* shared slot above. If each app read the whole list, the live app's shutdown
|
|
2646
|
+
* would close the candidate's pool and the candidate's would close the live
|
|
2647
|
+
* app's. Taking the declarations leaves each app holding exactly its own.
|
|
2648
|
+
*
|
|
2649
|
+
* A start hook that throws REFUSES THE BOOT — with the hook's name in the
|
|
2650
|
+
* message — after releasing whatever the earlier hooks already opened. Serving
|
|
2651
|
+
* from a half-initialised app is the silence this whole surface replaces, and a
|
|
2652
|
+
* boot that dies holding an open pool is the leak it replaces.
|
|
2653
|
+
*/
|
|
2654
|
+
declare function __runStartHooks(): Promise<ShutdownRunner>;
|
|
2655
|
+
/** Drop every declaration. For tests, which declare repeatedly in one process.
|
|
2656
|
+
* NOT part of the public author-facing API. */
|
|
2657
|
+
declare function __resetLifecycleHooks(): void;
|
|
2658
|
+
/**
|
|
2659
|
+
* Build the `.tables` accessor for the top-level `Database`. Each
|
|
2660
|
+
* `tables.<name>` access returns a small object that forwards the five CRUD ops
|
|
2661
|
+
* to the underlying client using `name` as the string table identifier. The
|
|
2662
|
+
* shapes are typed against the generated `palbase-env.d.ts` (`EnvTables`); at
|
|
2663
|
+
* runtime they are plain string-keyed calls, so no schema value is needed here.
|
|
2664
|
+
*
|
|
2665
|
+
* Returns `EnvTables` — TS cannot infer the mapped type through the Proxy, so
|
|
2666
|
+
* a single structural narrowing names the surface (the proxy returns a
|
|
2667
|
+
* correctly-shaped accessor for whatever string member is read).
|
|
2668
|
+
*/
|
|
2669
|
+
/** T018 (C-8): similar/recommend'in string-keyed yüzü. DBOps'a (endpoint.ts)
|
|
2670
|
+
* BİLEREK eklenmedi — search-param imza üçlüsü (engine/db + typed-db +
|
|
2671
|
+
* endpoint) büyümesin: proxy dispatch runtime'da engine ops'una zaten ulaşır,
|
|
2672
|
+
* derleme güvenliğini typed yüzey (EnvTypedTable) verir. */
|
|
2673
|
+
interface RecoOps {
|
|
2674
|
+
similar(table: string, id: string, params?: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
2675
|
+
recommend(table: string, params: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* The project's own Postgres (pgx, schema `env_<envId>`).
|
|
2679
|
+
*
|
|
2680
|
+
* Typed by default: `Database.tables.<name>.insert({...})` is typed against
|
|
2681
|
+
* the project's generated `palbase-env.d.ts` with NO import and NO generic.
|
|
2682
|
+
* The raw string ops (`query`/`insert`/`update`/`delete`/`findById`/`findMany`)
|
|
2683
|
+
* are also available for dynamic table names and read-only SQL.
|
|
2684
|
+
*
|
|
2685
|
+
* RLS is enforced by default (the runtime runs each op as `authenticated` with
|
|
2686
|
+
* the verified user's claims). To bypass RLS, call `Database.asService()` —
|
|
2687
|
+
* explicit and greppable — which runs as the `service_role` (BYPASSRLS).
|
|
2688
|
+
*
|
|
2689
|
+
* @example
|
|
2690
|
+
* import { Database } from "@palbase/backend";
|
|
2691
|
+
*
|
|
2692
|
+
* const todo = await Database.public.todos.insert({ title: req.input.title });
|
|
2693
|
+
* todo.id; // string ✓
|
|
2694
|
+
* const rows = await Database.$query("SELECT id FROM todos WHERE done = $1", [false]);
|
|
2695
|
+
* const all = await Database.$asService().public.todos.findMany({}); // RLS bypass
|
|
2696
|
+
*/
|
|
2697
|
+
declare const Database: EnvTypedDatabase;
|
|
2698
|
+
/** Firestore-like document client (PalDocs). */
|
|
2699
|
+
declare const Documents: PalbaseDocsClient;
|
|
2700
|
+
/**
|
|
2701
|
+
* `buckets.<name>` — the storage twin of `Database.tables.<name>`, and the same
|
|
2702
|
+
* mechanism: `config/storage.ts` generates a `Buckets` augmentation into
|
|
2703
|
+
* `palbase-env.d.ts`, so a bucket name is a property with no import and no
|
|
2704
|
+
* generic, and a typo is a compile error.
|
|
2705
|
+
*
|
|
2706
|
+
* The intermediate `.buckets` is there for the reason `.tables` is: bucket
|
|
2707
|
+
* names must not share a namespace with the client's own methods, or the day
|
|
2708
|
+
* somebody declares a bucket called `bucket` the surface breaks.
|
|
2709
|
+
*/
|
|
2710
|
+
type EnvBuckets = {
|
|
2711
|
+
[K in keyof Buckets]: TypedBucketClient<Buckets[K] extends BucketTypes ? Buckets[K] : BucketTypes>;
|
|
2712
|
+
};
|
|
2713
|
+
/** One bucket, with its declared variant names in the type. */
|
|
2714
|
+
interface TypedBucketClient<B extends BucketTypes> extends Omit<PalbaseBucketClient, "getPublicUrl"> {
|
|
2715
|
+
/** The public URL for the object, or for one of THIS bucket's declared
|
|
2716
|
+
* renditions. A name the bucket does not declare is a compile error. */
|
|
2717
|
+
getPublicUrl(path: string, options?: {
|
|
2718
|
+
variant?: B["variants"];
|
|
2719
|
+
}): string;
|
|
2720
|
+
}
|
|
2721
|
+
/**
|
|
2722
|
+
* Object storage: buckets, objects, renditions and signed URLs.
|
|
2723
|
+
*
|
|
2724
|
+
* `Storage.buckets.posts.upload(...)` is the typed path. `Storage.bucket(name)`
|
|
2725
|
+
* remains for a name computed at runtime — rare, and it gives up the typing,
|
|
2726
|
+
* which is the honest trade rather than a second blessed way to do it.
|
|
2727
|
+
*/
|
|
2728
|
+
declare const Storage: PalbaseStorageClient & {
|
|
2729
|
+
buckets: EnvBuckets;
|
|
2730
|
+
};
|
|
2731
|
+
/** JSON-typed cache (get/set/incr/getOrSet). */
|
|
2732
|
+
declare const Cache: CacheClient;
|
|
2733
|
+
/**
|
|
2734
|
+
* This tenant's secrets — API keys, provider credentials, signing material the
|
|
2735
|
+
* BACKEND owns.
|
|
2736
|
+
*
|
|
2737
|
+
* `await Secrets.get("STRIPE_KEY")` in a handler. There is no `.env` to read
|
|
2738
|
+
* and no file to mount: a secret is written through the vault's authenticated
|
|
2739
|
+
* API (or the CLI/MCP that calls it), and this is how the deployed code reads
|
|
2740
|
+
* it back. A name this tenant has not set answers null — as does every name
|
|
2741
|
+
* the STACK holds, because no route returns a platform secret's value at all.
|
|
2742
|
+
*/
|
|
2743
|
+
declare const Secrets: SecretsService;
|
|
2744
|
+
/** Structured logger. */
|
|
2745
|
+
declare const Log: Logger;
|
|
2746
|
+
/** Push / email / SMS / in-app notifications. */
|
|
2747
|
+
declare const Notifications: PalbaseNotificationsClient;
|
|
2748
|
+
/**
|
|
2749
|
+
* Feature flags.
|
|
2750
|
+
*
|
|
2751
|
+
* Mirrors the `Database` / `Database.asService()` model. The default surface is
|
|
2752
|
+
* RLS-equivalent for flags: reads resolve against the CURRENT request user and
|
|
2753
|
+
* `Flags.setOverride(key, value)` writes an override for that same signed-in
|
|
2754
|
+
* user (no userId argument, no admin power). Cross-user admin writes
|
|
2755
|
+
* (`setOverrideForUser`, …) live behind `Flags.asService()` — explicit and
|
|
2756
|
+
* greppable, just like `Database.asService()`.
|
|
2757
|
+
*
|
|
2758
|
+
* @example
|
|
2759
|
+
* import { Flags } from "@palbase/backend";
|
|
2760
|
+
*
|
|
2761
|
+
* if (await Flags.isEnabled("new_checkout")) { ... } // current user
|
|
2762
|
+
* await Flags.setOverride("new_checkout", true); // current user
|
|
2763
|
+
* await Flags.asService().setOverrideForUser("u_9", "x", true); // cross-user
|
|
2764
|
+
*/
|
|
2765
|
+
declare const Flags: PalbaseFlagsClient;
|
|
2766
|
+
/**
|
|
2767
|
+
* The Realtime broadcast singleton for the current request scope. Backend-side
|
|
2768
|
+
* Realtime is BROADCAST-ONLY (a stateless handler can push but not subscribe —
|
|
2769
|
+
* `subscribe()` lives on the client SDK's `pb.realtime`). Fire-and-forget:
|
|
2770
|
+
* `broadcast` resolves once accepted (or with an `error`), never blocking the
|
|
2771
|
+
* handler on subscribers.
|
|
2772
|
+
*
|
|
2773
|
+
* @example
|
|
2774
|
+
* import { Realtime } from "@palbase/backend";
|
|
2775
|
+
*
|
|
2776
|
+
* await Realtime.broadcast("room:42", "message", { text, from: user.id });
|
|
2777
|
+
*/
|
|
2778
|
+
declare const Realtime: PalbaseRealtimeClient;
|
|
2779
|
+
|
|
2780
|
+
/**
|
|
2781
|
+
* typed-db.ts — Task 2: TypedDB schema-derived insert/row shapes.
|
|
2782
|
+
*
|
|
2783
|
+
* Derives INSERT and full-row TypeScript types from a `defineSchema()` result
|
|
2784
|
+
* and wraps the untyped runtime `DBClient` with a typed facade.
|
|
2785
|
+
*
|
|
2786
|
+
* No value-any. No `as unknown as X`. The two narrow `as` casts in
|
|
2787
|
+
* `makeTypedTable` are safe because:
|
|
2788
|
+
* - `data as Record<string, unknown>`: InsertShape<T> maps string keys to
|
|
2789
|
+
* typed values; all value types are subsets of `unknown`, so the cast is
|
|
2790
|
+
* structurally sound.
|
|
2791
|
+
* - `result as RowShape<T>`: The runtime DBClient returns `Record<string,
|
|
2792
|
+
* unknown>` which is the erased form of the typed row; we're narrowing back
|
|
2793
|
+
* to the precise shape that the schema declared.
|
|
2794
|
+
* Both casts are narrowing only (not widening) and correctness is guaranteed
|
|
2795
|
+
* by the schema the caller provides.
|
|
2796
|
+
*/
|
|
2797
|
+
|
|
2798
|
+
/** Keys of C whose columns are required on INSERT (not nullable, no default). */
|
|
2799
|
+
type RequiredKeys<C> = {
|
|
2800
|
+
[K in keyof C]: ColIsOptionalOnInsert<C[K]> extends true ? never : K;
|
|
2801
|
+
}[keyof C];
|
|
2802
|
+
/** Keys of C whose columns are optional on INSERT (nullable or has a default). */
|
|
2803
|
+
type OptionalKeys<C> = {
|
|
2804
|
+
[K in keyof C]: ColIsOptionalOnInsert<C[K]> extends true ? K : never;
|
|
2805
|
+
}[keyof C];
|
|
2806
|
+
/**
|
|
2807
|
+
* The TypeScript type for an INSERT payload for table `T`.
|
|
2808
|
+
* - Required: columns that are NOT NULL and have no DB-level default.
|
|
2809
|
+
* - Optional: columns that are nullable or carry a default.
|
|
2810
|
+
*
|
|
2811
|
+
* When all columns are optional, `RequiredKeys<C>` resolves to `never` and
|
|
2812
|
+
* the first part becomes `{}`, which is a neutral element for `&`.
|
|
2813
|
+
*/
|
|
2814
|
+
type InsertShape<T extends TableDef> = {
|
|
2815
|
+
[K in RequiredKeys<T["columns"]>]: ColValue<T["columns"][K]>;
|
|
2816
|
+
} & {
|
|
2817
|
+
[K in OptionalKeys<T["columns"]>]?: ColValue<T["columns"][K]>;
|
|
2818
|
+
};
|
|
2819
|
+
/**
|
|
2820
|
+
* The TypeScript type for a full row returned by the DB for table `T`.
|
|
2821
|
+
* Every column is present; nullable columns resolve to `T | null`.
|
|
2822
|
+
*/
|
|
2823
|
+
type RowShape<T extends TableDef> = {
|
|
2824
|
+
[K in keyof T["columns"]]: ColValue<T["columns"][K]>;
|
|
2825
|
+
};
|
|
2826
|
+
/** A typed table accessor that mirrors the runtime DBClient surface. */
|
|
2827
|
+
interface TypedTable<T extends TableDef> {
|
|
2828
|
+
insert(data: InsertShape<T>): Promise<RowShape<T>>;
|
|
2829
|
+
put(q: {
|
|
2830
|
+
data: InsertShape<T>;
|
|
2831
|
+
onConflict: readonly string[];
|
|
2832
|
+
}): Promise<RowShape<T>>;
|
|
2833
|
+
/** Update the row by id; resolves to the updated row, or `null` if no row
|
|
2834
|
+
* matched (absent or RLS-hidden) — an idempotent outcome, mirroring
|
|
2835
|
+
* `findById`. The runtime returns a null row rather than throwing. */
|
|
2836
|
+
update(q: {
|
|
2837
|
+
where: {
|
|
2838
|
+
id: string;
|
|
2839
|
+
};
|
|
2840
|
+
set: Partial<InsertShape<T>>;
|
|
2841
|
+
}): Promise<RowShape<T> | null>;
|
|
2842
|
+
delete(id: string): Promise<void>;
|
|
2843
|
+
findById(id: string): Promise<RowShape<T> | null>;
|
|
2844
|
+
/** Rows matching the filter. Operators, ordering and paging are the ENGINE's
|
|
2845
|
+
* surface — this declaration is what makes them callable. */
|
|
2846
|
+
findMany<K extends keyof RowShape<T> = keyof RowShape<T>>(q?: QueryInput<RowShape<T>, K>): Promise<Pick<RowShape<T>, K>[]>;
|
|
2847
|
+
/**
|
|
2848
|
+
* Update every matching row in one statement; an empty filter is refused.
|
|
2849
|
+
*
|
|
2850
|
+
* **0 satır dönmesi hata DEĞİL, başarı da değil** (FR-014): koşulu `where`'e
|
|
2851
|
+
* koyup dönen diziyi kontrol etmek, "önce oku sonra yaz"ın yarış koşulu
|
|
2852
|
+
* olmayan hâlidir.
|
|
2853
|
+
*
|
|
2854
|
+
* ```ts
|
|
2855
|
+
* const [row] = await Database.public.accounts.updateMany({
|
|
2856
|
+
* where: { id, balance: { gte: amount } },
|
|
2857
|
+
* set: { balance: decrement(amount) },
|
|
2858
|
+
* });
|
|
2859
|
+
* if (row === undefined) throw new Conflict("yetersiz bakiye");
|
|
2860
|
+
* ```
|
|
2861
|
+
*/
|
|
2862
|
+
updateMany(q: MutateInput<RowShape<T>, InsertShape<T>>): Promise<RowShape<T>[]>;
|
|
2863
|
+
/** Delete every matching row; resolves to how many. Empty filter refused. */
|
|
2864
|
+
deleteMany(q: {
|
|
2865
|
+
where: WhereFilter<RowShape<T>> | SqlFragment;
|
|
2866
|
+
}): Promise<number>;
|
|
2867
|
+
/** How many rows match. An empty filter is legitimate: counting is a read. */
|
|
2868
|
+
count(q?: {
|
|
2869
|
+
where?: WhereFilter<RowShape<T>> | SqlFragment;
|
|
2870
|
+
}): Promise<number>;
|
|
2871
|
+
}
|
|
2872
|
+
/** A typed DB facade covering all tables declared in schema `S`. */
|
|
2873
|
+
interface TypedDB<S extends SchemaDef> {
|
|
2874
|
+
tables: {
|
|
2875
|
+
[K in keyof S["tables"]]: TypedTable<S["tables"][K]>;
|
|
2876
|
+
};
|
|
2877
|
+
/** Run a transaction plan. See {@link EnvTypedDatabase.transaction}. */
|
|
2878
|
+
transaction<T>(fn: (tx: TypedTx<S>) => T extends Promise<unknown> ? never : T): Promise<Materialized<T>>;
|
|
2879
|
+
}
|
|
2880
|
+
/** The plan-building handle a `TypedDB<S>` transaction callback receives: the
|
|
2881
|
+
* schema's tables, expressed as plan operations rather than awaited calls.
|
|
2882
|
+
*
|
|
2883
|
+
* YALNIZ `tables`, ve ÇALIŞMA ZAMANI DA yalnız onu veriyor.
|
|
2884
|
+
*
|
|
2885
|
+
* Bir süre runtime `{ tables, public }` döndürüyordu ama tip yalnız `tables`
|
|
2886
|
+
* söylüyordu: `tx.public.x` derlenmiyor, koşsaydı çalışacaktı — tipin çalışma
|
|
2887
|
+
* zamanından AZ söylemesi (gözcü M-8). Tipe `public` EKLEMEK yanlış düzeltmeydi
|
|
2888
|
+
* ve `check:api` onu adıyla reddetti: `TypedTx` dışa açık, ve zorunlu bir üye
|
|
2889
|
+
* eklemek onu İNŞA EDEN müşterinin kodunu kırar — "the new shape demands more
|
|
2890
|
+
* than the recorded one". Yalan runtime'dan kaldırılarak kapatıldı; ikisi artık
|
|
2891
|
+
* aynı şeyi söylüyor. `Database.$transaction`'ın `tx.public`'i ayrı bir yüzey
|
|
2892
|
+
* (`TxPlan`) ve orada tip de runtime da onu taşıyor. */
|
|
2893
|
+
type TypedTx<S extends SchemaDef> = TxPlanHandle<{
|
|
2894
|
+
[K in keyof S["tables"]]: TxTable<RowShape<S["tables"][K]>, InsertShape<S["tables"][K]>>;
|
|
2895
|
+
}>;
|
|
2896
|
+
/**
|
|
2897
|
+
* Wraps a raw `DBClient` with the type-safe `TypedDB<S>` facade derived from
|
|
2898
|
+
* the provided schema. No behavior change for the direct ops — all calls
|
|
2899
|
+
* delegate to `raw` with the table name as a plain string.
|
|
2900
|
+
*
|
|
2901
|
+
* `transaction` does NOT delegate to a per-op client: the callback describes a
|
|
2902
|
+
* plan against a fresh {@link TxPlanBuilder}, and the whole plan travels in one
|
|
2903
|
+
* `raw.txPlan` call. The schema is used only for its table NAMES; the values
|
|
2904
|
+
* are typed by `S` at compile time and are plain strings at run time.
|
|
2905
|
+
*
|
|
2906
|
+
* The `as` casts are single structural narrowings from a dynamically-built
|
|
2907
|
+
* object to the precise mapped type (TS cannot infer the mapped-type result
|
|
2908
|
+
* through `Object.keys` iteration) — see the module-level doc comment.
|
|
2909
|
+
*/
|
|
2910
|
+
declare function makeTypedDB<S extends SchemaDef>(schema: S, raw: DBClient): TypedDB<S>;
|
|
2911
|
+
/** Bir where değeri: düz eşitlik YA DA operatör nesnesi (FR-016). */
|
|
2912
|
+
/**
|
|
2913
|
+
* Metin operatörleri YALNIZ metin kolonlarında görünür (FR-005).
|
|
2914
|
+
*
|
|
2915
|
+
* `contains` bir sayı kolonunda anlamsızdır ve onu tipte sunmak "ifade edilemez
|
|
2916
|
+
* kıl" ilkesinin tersidir: derleme anında yakalanabilecek bir hata çalışma
|
|
2917
|
+
* zamanına ertelenir. Koşullu tip ÖZYİNELEMESİZDİR (N-2).
|
|
2918
|
+
*/
|
|
2919
|
+
/**
|
|
2920
|
+
* Bir KOLONA yapılan referans (FR-011) — karşılaştırmanın sağ tarafında değer
|
|
2921
|
+
* yerine durabilen tek şey.
|
|
2922
|
+
*
|
|
2923
|
+
* Şekli okunabilir (`{ $col: "…" }`) ama KİMLİĞİ şekli değil: nesne
|
|
2924
|
+
* `Symbol.for("palbase.db.ref")` ile markalanır. Şekil tek başına yetseydi
|
|
2925
|
+
* güvenilmeyen bir istek gövdesi `{"$col":"tenant_id"}` gönderip kiracılık
|
|
2926
|
+
* predikatını `t."tenant_id" = t."tenant_id"` totolojisine çevirebilirdi —
|
|
2927
|
+
* ölçüldü (W2-A/C1). Marka enumerable olmadığı için JSON round-trip'i onu
|
|
2928
|
+
* düşürür; süreç içinde (transaction planı dahil) korunur.
|
|
2929
|
+
*/
|
|
2930
|
+
type ColRef<N extends string = string> = {
|
|
2931
|
+
readonly $col: N;
|
|
2932
|
+
};
|
|
2933
|
+
|
|
2934
|
+
/**
|
|
2935
|
+
* `total > amount_paid` — bugün bunu yazmanın tek yolu ham SQL'e düşmekti,
|
|
2936
|
+
* çünkü sağ taraf bir DEĞER değil.
|
|
2937
|
+
*
|
|
2938
|
+
* Ad literal olarak yakalanır (`const N`), ve doğruluğu ÇAĞRIDA değil
|
|
2939
|
+
* KULLANIM YERİNDE kanıtlanır: filtre `WhereFilter<Row>` beklediği için
|
|
2940
|
+
* `ColRef<"yok_boyle">` oraya atanamaz. Yani `col()`'a satır tipini elle
|
|
2941
|
+
* vermek gerekmiyor, yanlış ad yine de derleme hatası (P5).
|
|
2942
|
+
*
|
|
2943
|
+
* Dönen nesne MARKALIDIR (`brandRef`): telden gelen `{"$col":"x"}` bir kolon
|
|
2944
|
+
* referansı SAYILMAZ, "bilinmeyen operatör" diye reddedilir. Gerekçe
|
|
2945
|
+
* `db/input-guards.ts`'te ölçümüyle birlikte yazılı.
|
|
2946
|
+
*/
|
|
2947
|
+
declare function col<const N extends string>(name: N): ColRef<N>;
|
|
2948
|
+
/** Bu satırın kolonlarından birine referans. `Row` bilinmiyorsa hiçbiri. */
|
|
2949
|
+
/**
|
|
2950
|
+
* Bir kolonun karşılaştırma AİLESİ — ve neden PG tipi değil de bu.
|
|
2951
|
+
*
|
|
2952
|
+
* ÖLÇÜLDÜ (gözcü, canlı pg16): TS tarafı PG tipini TAŞIMIYOR. `numeric`,
|
|
2953
|
+
* `bigint`, `text`, `uuid` ve `timestamp` hepsi `string`'e, `integer` `number`'a
|
|
2954
|
+
* düşüyor (`env-gen.ts:30-60`). Bu yüzden TS tipine kurulan HİÇBİR kural doğru
|
|
2955
|
+
* olamaz — kural ne olursa olsun bir taraf yanlışa düşer:
|
|
2956
|
+
*
|
|
2957
|
+
* { qty: { gt: col("amount") } } integer ↔ numeric PG: GEÇERLİ
|
|
2958
|
+
* { total: { gt: col("note") } } integer ↔ text PG: GEÇERSİZ
|
|
2959
|
+
*
|
|
2960
|
+
* İkisi de TS'te "number ↔ string". Ayırt edecek bilgi tipte YOK.
|
|
2961
|
+
*
|
|
2962
|
+
* YANLIŞ RET, YANLIŞ KABULDEN KÖTÜDÜR — ve tercih buna göre yapıldı. Yanlış bir
|
|
2963
|
+
* ret meşru bir sorguyu İFADE EDİLEMEZ kılar, yani yazarı `$query`'ye düşürür:
|
|
2964
|
+
* bu yüzeyin kapatmak için var olduğu şey. Yanlış bir kabul ise Postgres'in
|
|
2965
|
+
* GÜRÜLTÜLÜ hatasına çıkar (`operator does not exist: integer > text`) — kötü
|
|
2966
|
+
* ama sessiz değil.
|
|
2967
|
+
*
|
|
2968
|
+
* O yüzden aile ayrımı yalnız TS'in GERÇEKTEN ayırdığı yerlerde yapılıyor:
|
|
2969
|
+
* `boolean`, vektör dizisi ve `Date` kendi başlarına; metin ve sayı ise TEK
|
|
2970
|
+
* ailede, çünkü aralarındaki PG ayrımı tipte yok.
|
|
2971
|
+
*
|
|
2972
|
+
* KALICI ÇÖZÜM tipte değil ÜRETİMDE: `env-gen` kolon başına bir PG-tip markası
|
|
2973
|
+
* bassın (`string & { readonly __pg?: "numeric" }`). O zaman `numeric ↔ integer`
|
|
2974
|
+
* kabul, `numeric ↔ text` ret olur ve ikisi de DOĞRU olur. Defterde teklif.
|
|
2975
|
+
*/
|
|
2976
|
+
type PgFamily<V> = [NonNullable<V>] extends [boolean] ? "bool" : [NonNullable<V>] extends [readonly (number | string)[]] ? "array" : [NonNullable<V>] extends [Date] ? "date" : [NonNullable<V>] extends [string | number] ? "scalar" : "opaque";
|
|
2977
|
+
/** `Row`'un `V` ile karşılaştırılabilir kolonları — aynı aileden olanlar. */
|
|
2978
|
+
type ColumnsComparableTo<Row, V> = {
|
|
2979
|
+
[K in keyof Row]-?: PgFamily<Row[K]> extends PgFamily<V> ? K : never;
|
|
2980
|
+
}[keyof Row];
|
|
2981
|
+
/**
|
|
2982
|
+
* Bu satırın kolonlarından birine referans. `Row` bilinmiyorsa hiçbiri.
|
|
2983
|
+
*
|
|
2984
|
+
* İKİNCİ PARAMETRE kolonun tipi. Verilmezse (`unknown`) KISIT YOKTUR — bu
|
|
2985
|
+
* `findMany`'nin `Row`'u bilmediği yolların ve jsonb kolonlarının hâli, ve
|
|
2986
|
+
* ikisinde de tipin söyleyebileceği bir şey yok.
|
|
2987
|
+
*/
|
|
2988
|
+
type ColRefOf<Row, V = unknown> = [keyof Row] extends [never] ? never : [unknown] extends [V] ? ColRef<Extract<keyof Row, string>> : ColRef<Extract<ColumnsComparableTo<Row, V>, string>>;
|
|
2989
|
+
/**
|
|
2990
|
+
* Doğrulanmış, GÖMÜLEBİLİR bir SQL parçası (FR-018, FR-019).
|
|
2991
|
+
*
|
|
2992
|
+
* Wire şekli `{ $sql: { text, values } }`: `text` parçalar hâlinde tutulur ve
|
|
2993
|
+
* değerler ARADA durur, çünkü birleştirilmiş tek bir string'in içinden hangi
|
|
2994
|
+
* kısmın kullanıcı verisi olduğunu bir daha kimse çıkaramaz — enjeksiyonun
|
|
2995
|
+
* doğduğu yer tam olarak orasıdır.
|
|
2996
|
+
*/
|
|
2997
|
+
type SqlFragment = {
|
|
2998
|
+
readonly $sql: {
|
|
2999
|
+
readonly text: readonly string[];
|
|
3000
|
+
readonly values: readonly unknown[];
|
|
3001
|
+
};
|
|
3002
|
+
};
|
|
3003
|
+
/**
|
|
3004
|
+
* Retryable bir hatada işlemi yeniden dener (FR-037).
|
|
3005
|
+
*
|
|
3006
|
+
* D-016: retry'SIZ bir izolasyon yükseltmesi sunmak defect'tir — kullanıcıya
|
|
3007
|
+
* çalışmayan bir düğme vermektir. `SerializationFailure`'ın metni
|
|
3008
|
+
* *"{ retry: n } verebilirsiniz"* diyor; bu fonksiyon o düğmenin gerçekten
|
|
3009
|
+
* çalışan yarısı.
|
|
3010
|
+
*
|
|
3011
|
+
* VARSAYILAN SIFIR. Sessiz bir varsayılan retry, idempotent OLMAYAN bir işlemi
|
|
3012
|
+
* çağıranın haberi olmadan iki kez çalıştırırdı — bir para transferini iki kez.
|
|
3013
|
+
* Tekrar denemek çağıranın kararı.
|
|
3014
|
+
*
|
|
3015
|
+
* Yalnız `isRetryable(e)` olan hatalar tekrarlanır. Küme dar tutuluyor: `23505`
|
|
3016
|
+
* buraya girseydi tekrar denemek aynı cevabı verir ve döngü boşuna dönerdi.
|
|
3017
|
+
*/
|
|
3018
|
+
declare function withRetry<T>(fn: () => Promise<T>, opts?: {
|
|
3019
|
+
retry?: number;
|
|
3020
|
+
}): Promise<T>;
|
|
3021
|
+
/**
|
|
3022
|
+
* K3: sorgudan KAÇMAK için değil, sorgunun İÇİNE girmek için kaçış kapağı.
|
|
3023
|
+
*
|
|
3024
|
+
* ```ts
|
|
3025
|
+
* Database.public.notes.findMany({
|
|
3026
|
+
* where: sqlFragment`similarity(title, ${q}) > 0.3`,
|
|
3027
|
+
* orderBy: { column: "created_at", direction: "desc" },
|
|
3028
|
+
* limit: 20,
|
|
3029
|
+
* })
|
|
3030
|
+
* ```
|
|
3031
|
+
*
|
|
3032
|
+
* `select` / `orderBy` / `limit` / RLS aynen çalışmaya devam eder — "yetmedi,
|
|
3033
|
+
* ham SQL'e geçeyim" anının bugünkü maliyeti tam olarak bunları kaybetmekti.
|
|
3034
|
+
*
|
|
3035
|
+
* TANIMLAYICI GÖMME YÜZEYİ YOK (`.raw` / `.unsafe` / `.identifier`). Drizzle'ın
|
|
3036
|
+
* CVE-2026-39356'sı ve Knex'in CVE-2019-10757'si tam o kapakta doğdu.
|
|
3037
|
+
*
|
|
3038
|
+
* Ve kapak fonksiyonun ÜSTÜNDE olmakla bitmiyordu: parça MARKALI değilken
|
|
3039
|
+
* `{"$sql":{"text":["1=1 -- pwned"],"values":[]}}` düz JSON'dan uydurulup
|
|
3040
|
+
* guard'ın tamamını atlıyordu (W2-B/C3, ölçüldü). Marka o kapağı wire ŞEKLİNE
|
|
3041
|
+
* de koyuyor — parça yalnız bu template tag'inden çıkabilir.
|
|
3042
|
+
*/
|
|
3043
|
+
declare function sqlFragment(strings: TemplateStringsArray, ...values: unknown[]): SqlFragment;
|
|
3044
|
+
type TextOps<V> = V extends string ? {
|
|
3045
|
+
contains?: string;
|
|
3046
|
+
icontains?: string;
|
|
3047
|
+
startsWith?: string;
|
|
3048
|
+
endsWith?: string;
|
|
3049
|
+
} : Record<never, never>;
|
|
3050
|
+
/** Operatör gövdesi, kolon-referans tipi DIŞARIDAN verilmiş hâliyle.
|
|
3051
|
+
*
|
|
3052
|
+
* `C` bir parametre, çünkü `ColRefOf<Row, V>` aksi hâlde altı operatör
|
|
3053
|
+
* konumunda AYRI AYRI instantiate edilirdi — kolon başına altı mapped type.
|
|
3054
|
+
* Böyle kolon başına BİR tane (N-2). */
|
|
3055
|
+
type WhereOpBody<V, C> = {
|
|
3056
|
+
gt?: V | C;
|
|
3057
|
+
gte?: V | C;
|
|
3058
|
+
lt?: V | C;
|
|
3059
|
+
lte?: V | C;
|
|
3060
|
+
neq?: V | C;
|
|
3061
|
+
in?: V[];
|
|
3062
|
+
/** `IS NULL` / `IS NOT NULL` (FR-006) — `= NULL` SQL'de her zaman UNKNOWN'dır. */
|
|
3063
|
+
isNull?: boolean;
|
|
3064
|
+
} & TextOps<V>;
|
|
3065
|
+
type WhereOp<V, Row = unknown> = WhereOpWith<V, ColRefOf<Row, V>>;
|
|
3066
|
+
/**
|
|
3067
|
+
* Operatör sözlüğü, kolon-referans/plan-tutamağı konumu DIŞARIDAN verilmiş.
|
|
3068
|
+
*
|
|
3069
|
+
* `export`, çünkü plan yüzeyi (`tx-plan.ts`) aynı sözlüğü `C = ColRefOf<…> |
|
|
3070
|
+
* Ref<…>` ile kuruyor: bir plan filtresi ÖNCEKİ bir işlemin sonucuna
|
|
3071
|
+
* bakabiliyor, `findMany` bakamıyor. Ref'i `V`'ye eklemek YANLIŞ olurdu —
|
|
3072
|
+
* `TextOps<V>` `V extends string` diye soruyor ve `string | Ref<string>` o
|
|
3073
|
+
* soruya HAYIR der, yani `contains`/`startsWith` sessizce KAYBOLURDU.
|
|
3074
|
+
*/
|
|
3075
|
+
type WhereOpWith<V, C> = V | C | WhereOpBody<V, C>;
|
|
3076
|
+
/**
|
|
3077
|
+
* A filter over a row: every field optional, each one a plain value (equality)
|
|
3078
|
+
* or an operator object. THE filter language — `findMany`, `updateMany`,
|
|
3079
|
+
* `deleteMany` and `count` all take this one, because two spellings of a filter
|
|
3080
|
+
* is how the two come to disagree.
|
|
3081
|
+
*/
|
|
3082
|
+
type WhereFilter<Row> = {
|
|
3083
|
+
[K in keyof Row]?: WhereOp<Row[K], Row>;
|
|
3084
|
+
} & {
|
|
3085
|
+
/**
|
|
3086
|
+
* Boolean bileşimi (FR-007). Dallar aynı filtre dilidir — iki yazım olmaz.
|
|
3087
|
+
*
|
|
3088
|
+
* ÖZYİNELEME BURADA BAŞLIYOR ve N-2'nin izlediği şey tam olarak budur:
|
|
3089
|
+
* Kysely'nin TS7'de 9,8M instantiation üreten vakası derin generic
|
|
3090
|
+
* özyinelemeydi. Burada derinlik yazarın filtresi kadardır ve pratikte
|
|
3091
|
+
* bir-iki seviyedir; tip maliyeti T001'in tavanına karşı ölçülür.
|
|
3092
|
+
*/
|
|
3093
|
+
OR?: WhereFilter<Row>[];
|
|
3094
|
+
AND?: WhereFilter<Row>[];
|
|
3095
|
+
NOT?: WhereFilter<Row>;
|
|
3096
|
+
};
|
|
3097
|
+
/**
|
|
3098
|
+
* Ordering and paging for a read.
|
|
3099
|
+
*
|
|
3100
|
+
* `column` is `keyof Row`, not `string`: a mistyped column name is a compile
|
|
3101
|
+
* error here rather than a runtime rejection three layers down. `offset`
|
|
3102
|
+
* without `limit` is refused by the engine — a page with no size is not a page.
|
|
3103
|
+
*/
|
|
3104
|
+
type OrderBySpec<Row> = {
|
|
3105
|
+
column: Extract<keyof Row, string>;
|
|
3106
|
+
direction?: "asc" | "desc";
|
|
3107
|
+
/** NULL'ların yeri (FR-008). Verilmezse Postgres varsayılanı geçerlidir ve o
|
|
3108
|
+
* varsayılan YÖNE GÖRE DEĞİŞİR: ASC'de NULLS LAST, DESC'te NULLS FIRST. */
|
|
3109
|
+
nulls?: "first" | "last";
|
|
3110
|
+
};
|
|
3111
|
+
type FindManyOpts<Row> = {
|
|
3112
|
+
/** Tek sıralama ya da SIRALI liste — sıra korunur (FR-008). */
|
|
3113
|
+
orderBy?: OrderBySpec<Row> | OrderBySpec<Row>[];
|
|
3114
|
+
limit?: number;
|
|
3115
|
+
offset?: number;
|
|
3116
|
+
};
|
|
3117
|
+
/**
|
|
3118
|
+
* Bir okumanın TEK parametresi (FR-004b).
|
|
3119
|
+
*
|
|
3120
|
+
* `where` bir ALAN, konumsal bir argüman değil. Kullanıcının kararı: "deterministik
|
|
3121
|
+
* olması lazım, tek düzlem" — aynı bilginin iki yere dağılması, alanlar çoğaldıkça
|
|
3122
|
+
* (select/include/orderBy) "hangisi nereye" sorusunu ezberlenecek bir şeye çevirir.
|
|
3123
|
+
* Prisma ve Drizzle de tek obje kullanıyor. Bedeli açık ve kabul edildi: en sık
|
|
3124
|
+
* kullanım uzuyor — `findMany({ where: { owner: uid } })`.
|
|
3125
|
+
*
|
|
3126
|
+
* ÖZYİNELEMESİZ kalmak zorunda (N-2): tip maliyeti şekilden değil özyineleme
|
|
3127
|
+
* derinliğinden geliyor (Kysely'nin TS7'de 9,8M instantiation vakası).
|
|
3128
|
+
*/
|
|
3129
|
+
/**
|
|
3130
|
+
* `has` — İLİŞKİ ÜZERİNDEN SÜZME, JOIN'in ihtiyaç duyulmayan hâli.
|
|
3131
|
+
*
|
|
3132
|
+
* ```ts
|
|
3133
|
+
* Database.public.interests.findMany({
|
|
3134
|
+
* where: { has: { user_interests: { user_id: uid } } },
|
|
3135
|
+
* orderBy: [{ column: "sort_order" }, { column: "name" }],
|
|
3136
|
+
* })
|
|
3137
|
+
* ```
|
|
3138
|
+
* → `… WHERE t."id" IN (SELECT r1."interest_id" FROM "user_interests" r1
|
|
3139
|
+
* WHERE r1."user_id" = $1)`
|
|
3140
|
+
*
|
|
3141
|
+
* İlişki ADLARI ve gittikleri kolonlar `palbase-env.d.ts`'in ZATEN bastığı
|
|
3142
|
+
* `relations` bloğundan geliyor — yabancı anahtarlardan türetiliyorlar, yazar
|
|
3143
|
+
* hiçbir kolon adı yazmıyor. O blok üretiliyordu ve HİÇBİR okuma onu
|
|
3144
|
+
* kullanmıyordu; `has` onun karşılığı.
|
|
3145
|
+
*
|
|
3146
|
+
* İç filtre AYNI dil: bir tablo ötesinde de `gt`, `icontains`, `OR`, ve iç içe
|
|
3147
|
+
* `has` yazılabiliyor.
|
|
3148
|
+
*/
|
|
3149
|
+
/**
|
|
3150
|
+
* İlişkinin HEDEF TABLOSU — anahtar ŞEMA NİTELİKLİ gelir.
|
|
3151
|
+
*
|
|
3152
|
+
* `buildRelations` `to`'yu `qualifiedTableKey` ile yazıyor: public için düz ad
|
|
3153
|
+
* (`"interests"`), başka şema için noktalı (`"billing.invoices"`). `Tables`
|
|
3154
|
+
* yalnız public'i taşır, diğerleri `Schemas`'tadır — noktalı anahtarı doğrudan
|
|
3155
|
+
* `Tables`'ta aramak HER ZAMAN ıskalar ve iç filtre sessizce `Record<string,
|
|
3156
|
+
* unknown>`'a düşerdi: bilinmeyen kolon da, yanlış tip de derlenirdi (ölçüldü).
|
|
3157
|
+
*/
|
|
3158
|
+
type RelatedTable<To> = To extends `${infer S}.${infer T}` ? S extends keyof Schemas ? T extends keyof Schemas[S] ? Schemas[S][T] : never : never : To extends keyof Tables ? Tables[To] : never;
|
|
3159
|
+
type RelatedRow<To> = RelatedTable<To> extends {
|
|
3160
|
+
row: infer R;
|
|
3161
|
+
} ? R : Record<string, unknown>;
|
|
3162
|
+
type RelatedRels<To> = RelatedTable<To> extends {
|
|
3163
|
+
relations: infer R;
|
|
3164
|
+
} ? R : unknown;
|
|
3165
|
+
/**
|
|
3166
|
+
* İç içe `has` ÜÇ seviyeyle sınırlı.
|
|
3167
|
+
*
|
|
3168
|
+
* Üretilen `relations` bloğu ÇİFT YÖNLÜ: `customers.invoices` ile
|
|
3169
|
+
* `invoices.customer` birbirini gösteriyor, yani grafiğin kendisi döngülü.
|
|
3170
|
+
* Sınırsız açılım bu döngüyü sonsuz bir tipe çevirirdi — TypeScript'in
|
|
3171
|
+
* "excessively deep" hatası, yazılan sorgunun karmaşıklığından değil ŞEMANIN
|
|
3172
|
+
* şeklinden gelirdi. Üç seviye, ölçtüğümüz tüm gerçek sorguların üstünde.
|
|
3173
|
+
*/
|
|
3174
|
+
type HasDepth = 0 | 1 | 2 | 3;
|
|
3175
|
+
type HasDec = [0, 0, 1, 2];
|
|
3176
|
+
type HasNest<Rels, D extends HasDepth> = [keyof Rels] extends [never] ? unknown : D extends 0 ? unknown : {
|
|
3177
|
+
has?: HasFilter<Rels, HasDec[D]>;
|
|
3178
|
+
};
|
|
3179
|
+
type HasFilter<Rels, D extends HasDepth = 3> = {
|
|
3180
|
+
[R in keyof Rels]?: Rels[R] extends {
|
|
3181
|
+
to: infer To;
|
|
3182
|
+
} ? WhereFilter<RelatedRow<To>> & HasNest<RelatedRels<To>, D> : never;
|
|
3183
|
+
};
|
|
3184
|
+
/** İlişki bilgisi taşıyan bir tablo tipinin filtre yüzeyi. */
|
|
3185
|
+
type WhereWithRelations<Row, Rels> = WhereFilter<Row> & {
|
|
3186
|
+
has?: HasFilter<Rels>;
|
|
3187
|
+
};
|
|
3188
|
+
/**
|
|
3189
|
+
* `has` dalı — SADECE `has`, satır filtresi olmadan.
|
|
3190
|
+
*
|
|
3191
|
+
* Plan yolunun filtre dili (`TxWhere`) düz op'unkiyle aynı değil: orada bir
|
|
3192
|
+
* alan ÖNCEKİ bir işlemin satırına referans (`Ref`) taşıyabiliyor. `has`'ı
|
|
3193
|
+
* plana taşımak için o dili kopyalamak gerekmiyor — kesişimle EKLENİYOR.
|
|
3194
|
+
* İlişki yoksa `unknown` dönüyor, çünkü `X & unknown = X`: dal yok olur ve
|
|
3195
|
+
* `has` fazla-alan denetimine takılır.
|
|
3196
|
+
*/
|
|
3197
|
+
type HasOnly<Rels> = [unknown] extends [Rels] ? unknown : [keyof Rels] extends [never] ? unknown : {
|
|
3198
|
+
has?: HasFilter<Rels>;
|
|
3199
|
+
};
|
|
3200
|
+
/**
|
|
3201
|
+
* `has` dalı NE ZAMAN var olur.
|
|
3202
|
+
*
|
|
3203
|
+
* İlişkisi OLMAYAN bir tabloda `has` hiç yazılamamalı. `HasFilter<{}>` boş bir
|
|
3204
|
+
* nesne tipidir ve boş nesne tipi HER nesneyi kabul eder — yani dalı koşulsuz
|
|
3205
|
+
* eklemek, ilişkisiz bir tabloda `has: { neyse_ne: {} }`'yi SESSİZCE geçirirdi
|
|
3206
|
+
* (ölçüldü: kapı yeşil, çalışma anında "böyle bir ilişki yok" hatası).
|
|
3207
|
+
*/
|
|
3208
|
+
type HasBranch<Row, Rels> = [unknown] extends [Rels] ? never : [keyof Rels] extends [never] ? never : WhereWithRelations<Row, Rels>;
|
|
3209
|
+
type QueryInput<Row, K extends keyof Row = keyof Row, Rels = unknown> = {
|
|
3210
|
+
/**
|
|
3211
|
+
* Filtre — ya tipli filtre dili ya da bir `sqlFragment` (FR-018). İkisi de
|
|
3212
|
+
* AYNI alandır: kademe atlamak bir parametre değişikliği, ayrı bir çağrı
|
|
3213
|
+
* yolu değil (P1).
|
|
3214
|
+
*/
|
|
3215
|
+
where?: WhereFilter<Row> | SqlFragment | HasBranch<Row, Rels>;
|
|
3216
|
+
/**
|
|
3217
|
+
* Projeksiyon (FR-009): yalnız bu kolonlar çekilir ve DÖNÜŞ TİPİ buna daralır.
|
|
3218
|
+
*
|
|
3219
|
+
* Tip `Pick<Row, K>` ile TÜRETİLİR, üretilmez: `select` kombinasyonları için
|
|
3220
|
+
* tip basmak, üretilen `.d.ts`'i kombinatoryal olarak şişirirdi (N-1: tablo
|
|
3221
|
+
* başına ≤ 25 satır).
|
|
3222
|
+
*/
|
|
3223
|
+
select?: readonly K[];
|
|
3224
|
+
} & FindManyOpts<Row>;
|
|
3225
|
+
/** Bir yazmanın TEK parametresi: neyi (`where`) neye çevirdiğin (`set`). */
|
|
3226
|
+
type MutateInput<Row, Insert, Rels = unknown> = {
|
|
3227
|
+
where: WhereFilter<Row> | SqlFragment | HasBranch<Row, Rels>;
|
|
3228
|
+
set: SetShape<Insert>;
|
|
3229
|
+
};
|
|
3230
|
+
/**
|
|
3231
|
+
* `set`'e yazılabilen değer (FR-012): kolonun kendi tipi ya da — sayısal
|
|
3232
|
+
* kolonlarda — kolonun ŞU ANKİ değerini okuyan bir ifade.
|
|
3233
|
+
*
|
|
3234
|
+
* `increment()` metin ya da boolean kolonda YOK: "kolona ekle"nin orada bir
|
|
3235
|
+
* anlamı olmadığı için ifade edilemez kılınıyor (P5 — anlatmak değil, ifade
|
|
3236
|
+
* edilemez kılmak). `numeric` kolonlar TypeScript'te `string` taşır, o yüzden
|
|
3237
|
+
* string de sayısal sayılır; ayrımı burada yapamayız, ama D-007 zaten miktarın
|
|
3238
|
+
* JS `number`'a hiç uğramamasını istiyor.
|
|
3239
|
+
*/
|
|
3240
|
+
type SetValue<V> = V | (NonNullable<V> extends number | string ? TxColumnExpr : never) | (NonNullable<V> extends Date ? TxNow : never);
|
|
3241
|
+
/** Bir update'in `set`'i: insert şeklinin herhangi bir alt kümesi, ifadelerle. */
|
|
3242
|
+
type SetShape<Insert> = {
|
|
3243
|
+
[K in keyof Insert]?: SetValue<Insert[K]>;
|
|
3244
|
+
};
|
|
3245
|
+
/** search() parametreleri, satır tipiyle koşullanmış (FR-013). `offset` BİLEREK yok (UD-013). */
|
|
3246
|
+
interface SearchParamsTyped<T extends TableTypes> {
|
|
3247
|
+
/** Metin sorgusu: FTS kolunu besler; embed beyanlıysa sorgu vektörü de bundan üretilir. */
|
|
3248
|
+
query?: string;
|
|
3249
|
+
/** Hazır sorgu vektörü — verilirse embed çağrısı olmaz (FR-025). */
|
|
3250
|
+
vector?: number[];
|
|
3251
|
+
/** `findMany`'nin filtre dili — İKİNCİ tip argümanıyla, yoksa `Row` sessizce
|
|
3252
|
+
* `unknown`'a düşer ve `col()` burada ifade edilemez olurdu. Motor `search`
|
|
3253
|
+
* için aynı `compileWhereBare`'i kullanıyor; yetenek orada (gözcü I7). */
|
|
3254
|
+
where?: {
|
|
3255
|
+
[K in keyof T["row"]]?: WhereOp<T["row"][K], T["row"]>;
|
|
3256
|
+
};
|
|
3257
|
+
/** default 20, tavan 100 (engine uygular). */
|
|
3258
|
+
limit?: number;
|
|
3259
|
+
/** Birden çok vektör kolonunda hedef seçimi (model geçişi, FR-013/using). */
|
|
3260
|
+
using?: string;
|
|
3261
|
+
mode?: "hybrid" | "text" | "vector";
|
|
3262
|
+
/** Nihai (RRF-sonrası) skor alt eşiği — süzme LIMIT'ten önce uygulanır (FR-001). */
|
|
3263
|
+
minScore?: number;
|
|
3264
|
+
/** Chunk-modunda satır başına en iyi blok sayısı (1..10, vars. 3; FR-015). */
|
|
3265
|
+
blocksPerRow?: number;
|
|
3266
|
+
/** Tazelik çürümesi: nihai skor RRF-sonrası exp(-ln(2)*yaş/halfLife) ile çarpılır;
|
|
3267
|
+
* field bir timestamp kolonu, halfLife "90s" | "15m" | "12h" | "30d" biçiminde (FR-004). */
|
|
3268
|
+
recency?: {
|
|
3269
|
+
field: Extract<keyof T["row"], string>;
|
|
3270
|
+
halfLife: string;
|
|
3271
|
+
};
|
|
3272
|
+
/** Satır-modunda FTS eşleşme vurgusu: sonuç satırına `_highlight` ekler;
|
|
3273
|
+
* chunk-modda no-op — bloklar zaten eşleşen kesittir (FR-025). */
|
|
3274
|
+
highlight?: boolean;
|
|
3275
|
+
/** Validity'li tabloda zaman penceresi: varsayılan yalnız güncel versiyon;
|
|
3276
|
+
* "all" tüm versiyonlar; {asOf} o anda geçerli olan (FR-029). */
|
|
3277
|
+
validity?: "all" | {
|
|
3278
|
+
asOf: string;
|
|
3279
|
+
};
|
|
3280
|
+
/** Alan-boost (FR-030): skor * (1 + w·x/(1+x)) — sayısal kolonla sınırlı
|
|
3281
|
+
* çarpan, dış servissiz; bileşim RRF → boost → recency → minScore. */
|
|
3282
|
+
boost?: {
|
|
3283
|
+
field: Extract<keyof T["row"], string>;
|
|
3284
|
+
weight: number;
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
/**
|
|
3288
|
+
* `facets()` dönüşü (FR-027, FR-058): kolon adı → o kolonun top-20 değeri ve
|
|
3289
|
+
* sayacı.
|
|
3290
|
+
*
|
|
3291
|
+
* Bu tip bir zamanlar `search()` dönüş DİZİSİNİN üstünde taşınıyordu; dizi-üstü
|
|
3292
|
+
* özellik `JSON.stringify`'da kayboluyor, yani bir kontrolcü onu döndürmeye
|
|
3293
|
+
* çalıştığında yanıt gövdesinde hiç görünmüyordu. Sayaçlar artık BAĞIMSIZ
|
|
3294
|
+
* dönüşle geliyor ve tip de o dönüşü adlandırıyor.
|
|
3295
|
+
*/
|
|
3296
|
+
type SearchFacets = Record<string, {
|
|
3297
|
+
value: string | null;
|
|
3298
|
+
count: number;
|
|
3299
|
+
}[]>;
|
|
3300
|
+
/** similar()/recommend() taşıyıcı opsiyonları (T018, FR-022): search'ün
|
|
3301
|
+
* paramlarından query/vector/mode düşer — hedef vektörü metodun kendisi
|
|
3302
|
+
* DB'den kurar; facets/highlight de düşer (T020) — engine bu ikisini
|
|
3303
|
+
* similar/recommend'e geçirmez, tip vaadi gerçekle aynı kalır. */
|
|
3304
|
+
type SimilarParamsTyped<T extends TableTypes> = Omit<SearchParamsTyped<T>, "query" | "vector" | "mode" | "facets" | "highlight">;
|
|
3305
|
+
/** recommend() parametreleri (T018, FR-023). */
|
|
3306
|
+
type RecommendParamsTyped<T extends TableTypes> = SimilarParamsTyped<T> & {
|
|
3307
|
+
/** Kaynak beğeniler — hedef vektör bunların DB-içi avg'ı; boş olamaz. */
|
|
3308
|
+
positive: string[];
|
|
3309
|
+
/** İtilen örnekler — hedef pos.v + (pos.v - neg.v) ile yönlenir. */
|
|
3310
|
+
negative?: string[];
|
|
3311
|
+
};
|
|
3312
|
+
/**
|
|
3313
|
+
* Tablonun ilişki bloğu — env `Tables` girdisinden.
|
|
3314
|
+
*
|
|
3315
|
+
* TEK yerde türetiliyor ve HER op'a aynısı veriliyor: `has` yalnız `findMany`'de
|
|
3316
|
+
* olsaydı, "aynı filtre dili her op'ta" sözü ilk `deleteMany`'de kırılırdı.
|
|
3317
|
+
*/
|
|
3318
|
+
type RelsOf<T> = T extends {
|
|
3319
|
+
relations: infer R;
|
|
3320
|
+
} ? R : unknown;
|
|
3321
|
+
/** Temel tablo erişimcisi — search'süz beş op. */
|
|
3322
|
+
interface EnvTypedTableBase<T extends TableTypes> {
|
|
3323
|
+
insert(data: T["insert"]): Promise<T["row"]>;
|
|
3324
|
+
/**
|
|
3325
|
+
* Bir idempotency anahtarını sahiplen (FR-033).
|
|
3326
|
+
*
|
|
3327
|
+
* ```ts
|
|
3328
|
+
* const { inserted, row } = await Database.public.payments.claim(
|
|
3329
|
+
* { idem_key: req.headers["idempotency-key"] },
|
|
3330
|
+
* { amount, user_id },
|
|
3331
|
+
* );
|
|
3332
|
+
* if (!inserted) return row; // aynı istek ikinci kez geldi
|
|
3333
|
+
* ```
|
|
3334
|
+
*
|
|
3335
|
+
* `inserted: false` bir HATA DEĞİL: dönen satır ilk çağrının satırıdır.
|
|
3336
|
+
* İlk argüman satırı BULAN alanlar, ikincisi yalnız yazılanlar — ikinci çağrı
|
|
3337
|
+
* farklı bir yük gönderse bile satır anahtarla bulunur.
|
|
3338
|
+
*/
|
|
3339
|
+
claim(unique: Partial<T["insert"]>, extra?: Partial<T["insert"]>): Promise<{
|
|
3340
|
+
inserted: boolean;
|
|
3341
|
+
row: T["row"];
|
|
3342
|
+
}>;
|
|
3343
|
+
/**
|
|
3344
|
+
* Satırı yaz; `onConflict` kolonlarında çakışırsa ÜZERİNE yaz (FR-034).
|
|
3345
|
+
*
|
|
3346
|
+
* ```ts
|
|
3347
|
+
* await Database.public.settings.put({
|
|
3348
|
+
* data: { user_id, theme: "dark" },
|
|
3349
|
+
* onConflict: ["user_id"],
|
|
3350
|
+
* });
|
|
3351
|
+
* ```
|
|
3352
|
+
*
|
|
3353
|
+
* `onConflict` kolonları benzersiz bir kısıt ya da index taşımalı — Postgres
|
|
3354
|
+
* onlarla eşleştirir — ve güncellemeden dışlanırlar, çünkü eşleşen şey onlar.
|
|
3355
|
+
*
|
|
3356
|
+
* **`upsert`'ün yerine geldi ve adı bilerek değişti.** `upsert` tek isim
|
|
3357
|
+
* altında iki niyet taşıyordu: "yoksa ekle varsa güncelle" (bu) ve "idempotent
|
|
3358
|
+
* yaz" (artık {@link EnvTypedTableBase.claim}). İkincisi için ÖLÇÜLMÜŞ biçimde
|
|
3359
|
+
* yanlıştı: `DO UPDATE` ikinci çağrının verisiyle birincininkini EZİYOR
|
|
3360
|
+
* (10.00 → 999.00 ölçüldü). İki niyeti tek isimde tutmak, yanlış olanı
|
|
3361
|
+
* seçmeyi kolay yapıyordu.
|
|
3362
|
+
*/
|
|
3363
|
+
put(q: {
|
|
3364
|
+
data: T["insert"];
|
|
3365
|
+
onConflict: readonly Extract<keyof T["row"], string>[];
|
|
3366
|
+
}): Promise<T["row"]>;
|
|
3367
|
+
/** Update the row by id; resolves to the updated row, or `null` if no row
|
|
3368
|
+
* matched (absent or RLS-hidden) — an idempotent outcome, mirroring
|
|
3369
|
+
* `findById`. The runtime returns a null row rather than throwing. */
|
|
3370
|
+
update(q: {
|
|
3371
|
+
where: {
|
|
3372
|
+
id: string;
|
|
3373
|
+
};
|
|
3374
|
+
set: Partial<T["insert"]>;
|
|
3375
|
+
}): Promise<T["row"] | null>;
|
|
3376
|
+
delete(id: string): Promise<void>;
|
|
3377
|
+
findById(id: string): Promise<T["row"] | null>;
|
|
3378
|
+
/** Rows matching the filter. See {@link WhereFilter} / {@link FindManyOpts} —
|
|
3379
|
+
* this declaration is what makes the engine's operators callable. */
|
|
3380
|
+
findMany<K extends keyof T["row"] = keyof T["row"]>(q?: QueryInput<T["row"], K, RelsOf<T>>): Promise<Pick<T["row"], K>[]>;
|
|
3381
|
+
/**
|
|
3382
|
+
* Update every matching row in one statement; an empty filter is refused.
|
|
3383
|
+
*
|
|
3384
|
+
* **0 satır dönmesi hata DEĞİL, başarı da değil** (FR-014): koşulu `where`'e
|
|
3385
|
+
* koyup dönen diziyi kontrol etmek, "önce oku sonra yaz"ın yarış koşulu
|
|
3386
|
+
* olmayan hâlidir.
|
|
3387
|
+
*
|
|
3388
|
+
* ```ts
|
|
3389
|
+
* const [row] = await Database.public.accounts.updateMany({
|
|
3390
|
+
* where: { id, balance: { gte: amount } },
|
|
3391
|
+
* set: { balance: decrement(amount) },
|
|
3392
|
+
* });
|
|
3393
|
+
* if (row === undefined) throw new Conflict("yetersiz bakiye");
|
|
3394
|
+
* ```
|
|
3395
|
+
*/
|
|
3396
|
+
updateMany(q: MutateInput<T["row"], T["insert"], RelsOf<T>>): Promise<T["row"][]>;
|
|
3397
|
+
/** Delete every matching row; resolves to how many. Empty filter refused. */
|
|
3398
|
+
deleteMany(q: {
|
|
3399
|
+
where: WhereFilter<T["row"]> | SqlFragment | HasBranch<T["row"], RelsOf<T>>;
|
|
3400
|
+
}): Promise<number>;
|
|
3401
|
+
/** How many rows match. An empty filter is legitimate: counting is a read. */
|
|
3402
|
+
count(q?: {
|
|
3403
|
+
where?: WhereFilter<T["row"]> | SqlFragment | HasBranch<T["row"], RelsOf<T>>;
|
|
3404
|
+
}): Promise<number>;
|
|
3405
|
+
/** Validity'li tabloda satırın yeni versiyonu (FR-029, C-9): eski satır
|
|
3406
|
+
* kapanır (valid_to/superseded_by), yenisi TEK savepoint'te eklenir; dönüş
|
|
3407
|
+
* yeni satır. Validity beyanı olmayan tabloda adlandırılmış çalışma-zamanı
|
|
3408
|
+
* hatası — tip düzeyinde ayrım env `Tables` bayrağı taşımadığından yapılamaz. */
|
|
3409
|
+
supersede(id: string, row: T["insert"]): Promise<T["row"]>;
|
|
3410
|
+
}
|
|
3411
|
+
/** Tablo erişimcisi: env girdisi `searchable: true` taşıyorsa (vector kolonu ya da
|
|
3412
|
+
* search beyanı — env-gen üretir) `search()` üyesi VARDIR; yoksa üye hiç yoktur ve
|
|
3413
|
+
* çağrı derleme hatasıdır (FR-013). Yapısal koşul TableTypes'ı genişletmeden çalışır. */
|
|
3414
|
+
/**
|
|
3415
|
+
* `appendOnly` tabloda YAYIMLANMAYAN üyeler (FR-031).
|
|
3416
|
+
*
|
|
3417
|
+
* Altısı da ayrı ayrı: biri unutulursa append-only sözü o üye üzerinden sessizce
|
|
3418
|
+
* delinir. `insert` ve okuma üyeleri kalır — düzeltme SİLMEKLE değil, telafi
|
|
3419
|
+
* kaydı EKLEMEKLE yapılır (FR-032).
|
|
3420
|
+
*/
|
|
3421
|
+
type AppendOnlyForbidden = "update" | "updateMany" | "delete" | "deleteMany" | "put" | "supersede";
|
|
3422
|
+
type EnvTypedTable<T extends TableTypes> = (T extends {
|
|
3423
|
+
appendOnly: true;
|
|
3424
|
+
} ? Omit<EnvTypedTableBase<T>, AppendOnlyForbidden> : EnvTypedTableBase<T>) & (T extends {
|
|
3425
|
+
searchable: true;
|
|
3426
|
+
} ? {
|
|
3427
|
+
search(params: SearchParamsTyped<T>): Promise<Array<T["row"] & {
|
|
3428
|
+
_score: number;
|
|
3429
|
+
_highlight?: string;
|
|
3430
|
+
}>>;
|
|
3431
|
+
/** "Bu satıra benzeyenler" (FR-022): hedef vektör DB'den okunur,
|
|
3432
|
+
* kaynak satır sonuçta yoktur; id yoksa adlandırılmış hata. */
|
|
3433
|
+
similar(id: string, params?: SimilarParamsTyped<T>): Promise<Array<T["row"] & {
|
|
3434
|
+
_score: number;
|
|
3435
|
+
}>>;
|
|
3436
|
+
/** D-021: sayaçlar bağımsız dönüşle — search'ün dizi-üstü _facets'i
|
|
3437
|
+
* JSON.stringify'da kaybolur; ciddi sözleşme budur. */
|
|
3438
|
+
facets(params: {
|
|
3439
|
+
facets: Array<keyof T["row"] & string>;
|
|
3440
|
+
where?: Partial<T["row"]>;
|
|
3441
|
+
validity?: "all" | {
|
|
3442
|
+
asOf: string;
|
|
3443
|
+
};
|
|
3444
|
+
}): Promise<SearchFacets>;
|
|
3445
|
+
/** positive/negative beğenilerden öneri (FR-023): hedef vektör DB-içi
|
|
3446
|
+
* avg CTE'leriyle; kaynak id'ler sonuçta yoktur. */
|
|
3447
|
+
recommend(params: RecommendParamsTyped<T>): Promise<Array<T["row"] & {
|
|
3448
|
+
_score: number;
|
|
3449
|
+
}>>;
|
|
3450
|
+
} : Record<never, never>);
|
|
3451
|
+
/** The `tables` map exposed on `Database`/`tx`, keyed by the env `Tables`
|
|
3452
|
+
* interface. When no schema is declared `Tables` is empty, so `tables` is an
|
|
3453
|
+
* empty object — accessing `.tables.foo` is then a compile error (no member). */
|
|
3454
|
+
type EnvTables = {
|
|
3455
|
+
[K in keyof Tables]: EnvTypedTable<Tables[K]>;
|
|
3456
|
+
};
|
|
3457
|
+
/**
|
|
3458
|
+
* The project's NON-public schemas, keyed by schema name, each exposing its own
|
|
3459
|
+
* `tables` map — the shape `Database.schema("billing")` returns.
|
|
3460
|
+
*
|
|
3461
|
+
* The intermediate `tables` is there for the reason {@link EnvTables} sits under
|
|
3462
|
+
* `.tables`: a schema's table names must not share a namespace with anything the
|
|
3463
|
+
* accessor itself might grow.
|
|
3464
|
+
*
|
|
3465
|
+
* Empty by default. The generated `palbase-env.d.ts` augments `Schemas` with one
|
|
3466
|
+
* member per declared schema other than `public`, so a project that declares
|
|
3467
|
+
* none has `keyof Schemas = never` and every `schema(...)` call is a compile
|
|
3468
|
+
* error rather than a runtime surprise.
|
|
3469
|
+
*/
|
|
3470
|
+
/**
|
|
3471
|
+
* @deprecated ARTIK VAR OLMAYAN BİR ŞEKLİ tarif ediyor. Bu tip her şemayı
|
|
3472
|
+
* `{ tables: … }` altında gösteriyor; yüzey FR-001 ile `Database.<şema>.<tablo>`
|
|
3473
|
+
* oldu ve `.tables` ara katmanı KALKTI. Güncel şekil: {@link EnvSchemaSurface}.
|
|
3474
|
+
*
|
|
3475
|
+
* Kaldırılmadı çünkü dışa açık bir tip ve kaldırmak SEKİZİNCİ bir kırıcı olurdu;
|
|
3476
|
+
* ama bir tip, çalışma zamanının ÜRETMEDİĞİ bir şekli tarif ediyorsa yalan
|
|
3477
|
+
* söylüyor demektir — `const x: EnvSchemas["billing"] = Database.billing`
|
|
3478
|
+
* açıklanamayan bir derleme hatası verir. Bir sonraki kırıcı sürümde silinmeli
|
|
3479
|
+
* (defter D-18).
|
|
3480
|
+
*
|
|
3481
|
+
* Bu tipi ÜRETEN iki fonksiyon (`makeTablesAccessor`, `makeSchemaAccessor`)
|
|
3482
|
+
* ölçüldü: hiçbir yerden çağrılmıyorlardı ve silindiler. Canlı yol
|
|
3483
|
+
* `makeTypedSurface`'ın proxy'si.
|
|
3484
|
+
*/
|
|
3485
|
+
type EnvSchemas = {
|
|
3486
|
+
[S in keyof Schemas]: {
|
|
3487
|
+
tables: {
|
|
3488
|
+
[T in keyof Schemas[S]]: EnvTypedTable<Extract<Schemas[S][T], TableTypes>>;
|
|
3489
|
+
};
|
|
3490
|
+
};
|
|
3491
|
+
};
|
|
3492
|
+
/** The project's tables as PLAN operations, keyed by the env `Tables`
|
|
3493
|
+
* interface. The transaction twin of {@link EnvTables}. */
|
|
3494
|
+
/** `appendOnly` bir tabloda transaction planının da kaybettiği üyeler. Doğrudan
|
|
3495
|
+
* yüzeyin `AppendOnlyForbidden`'ının plan-yolu ikizi; adlar farklı çünkü plan
|
|
3496
|
+
* yüzeyinin üye adları farklı. */
|
|
3497
|
+
type TxAppendOnlyForbidden = "put" | "updateWhere" | "deleteWhere";
|
|
3498
|
+
/**
|
|
3499
|
+
* Plan yüzeyi de `appendOnly`'yi UYGULUYOR — ve uygulamıyor olması bir kusurdu.
|
|
3500
|
+
*
|
|
3501
|
+
* `EnvTypedTable` altı üyeyi Omit ediyordu, ama `TxTables` hiçbirini: yani
|
|
3502
|
+
* `tx.tables.entries.updateWhere(…)` ve `.put(…)` DERLENİYORDU. Tip bir şeyi
|
|
3503
|
+
* "ifade edilemez" ilan edip ikinci bir kapıda ifade edilebilir bırakırsa,
|
|
3504
|
+
* ilan yalandır (doğrulayıcı gözcü, FR-031).
|
|
3505
|
+
*
|
|
3506
|
+
* Motor da aynı üçünü adıyla reddediyor — tip atlanınca susmaması için.
|
|
3507
|
+
*/
|
|
3508
|
+
type TxTables = {
|
|
3509
|
+
[K in keyof Tables]: Tables[K] extends {
|
|
3510
|
+
appendOnly: true;
|
|
3511
|
+
} ? Omit<TxTable<Tables[K]["row"], Tables[K]["insert"], RelsOf<Tables[K]>>, TxAppendOnlyForbidden> : TxTable<Tables[K]["row"], Tables[K]["insert"], RelsOf<Tables[K]>>;
|
|
3512
|
+
};
|
|
3513
|
+
/** Bir şemanın tabloları PLAN operasyonları olarak — `TxTables`'ın public
|
|
3514
|
+
* DIŞI şemalar için ikizi. */
|
|
3515
|
+
type TxTablesOf<S> = {
|
|
3516
|
+
[T in keyof S]: Extract<S[T], TableTypes> extends {
|
|
3517
|
+
appendOnly: true;
|
|
3518
|
+
} ? Omit<TxTable<Extract<S[T], TableTypes>["row"], Extract<S[T], TableTypes>["insert"], RelsOf<Extract<S[T], TableTypes>>>, TxAppendOnlyForbidden> : TxTable<Extract<S[T], TableTypes>["row"], Extract<S[T], TableTypes>["insert"], RelsOf<Extract<S[T], TableTypes>>>;
|
|
3519
|
+
};
|
|
3520
|
+
/**
|
|
3521
|
+
* Plan tutamağının şema yüzeyi — `EnvSchemaSurface`'ın BİREBİR ikizi.
|
|
3522
|
+
*
|
|
3523
|
+
* NEDEN AYNI ŞEKİL: `Database.billing.invoices` yazılabiliyorken
|
|
3524
|
+
* `tx.billing.invoices` yazılamıyordu, yani `billing` şemasındaki iki tabloyu
|
|
3525
|
+
* TEK ATOMİK PLANDA yazmak imkânsızdı — yazar `$query`'ye düşüyor, tipi ve RLS
|
|
3526
|
+
* yardımını kaybediyordu. Bu yüzeyin kapatmak için var olduğu düşüşün ta
|
|
3527
|
+
* kendisi, ve GOAL'ün "ciddi ölçekli fintech" yarısının tam ortasında
|
|
3528
|
+
* (nihai inceleme I-6).
|
|
3529
|
+
*/
|
|
3530
|
+
type TxSchemaSurface = {
|
|
3531
|
+
public: TxTables;
|
|
3532
|
+
} & {
|
|
3533
|
+
[S in keyof Schemas]: TxTablesOf<Schemas[S]>;
|
|
3534
|
+
};
|
|
3535
|
+
/**
|
|
3536
|
+
* The handle a `Database.$transaction(…)` callback receives.
|
|
3537
|
+
*
|
|
3538
|
+
* Tables only — no `query`, no `findById`, no `asService`. A read whose value
|
|
3539
|
+
* the plan does not write belongs outside the transaction, where it costs one
|
|
3540
|
+
* round trip and is an ordinary value you can branch on.
|
|
3541
|
+
*
|
|
3542
|
+
* `tx.public.x` ve `tx.<şema>.x`, `Database` ile aynı şekil. `tx.tables.x`
|
|
3543
|
+
* public'in TAKMA ADI olarak duruyor: bu run'ın göç notu onu öğretiyor ve her
|
|
3544
|
+
* mevcut çağrı onu kullanıyor — kaldırmak sekizinci bir kırıcı olurdu ve
|
|
3545
|
+
* hiçbir şey kazandırmazdı.
|
|
3546
|
+
*/
|
|
3547
|
+
type TxPlan = TxPlanHandle<TxTables> & TxSchemaSurface;
|
|
3548
|
+
/**
|
|
3549
|
+
* The RLS-bypass sibling returned by `Database.$asService()`. Same typed surface
|
|
3550
|
+
* as {@link EnvTypedDatabase} — `tables`, the raw string ops, and a typed
|
|
3551
|
+
* `transaction` — but it does NOT re-expose `asService` (no double-bypass).
|
|
3552
|
+
* Every op it performs runs as the `service_role` (BYPASSRLS).
|
|
3553
|
+
*/
|
|
3554
|
+
type EnvServiceDatabase = EnvSchemaSurface & {
|
|
3555
|
+
/** Ham SQL. Generic verilmezse `unknown[]` döner (FR-021). */
|
|
3556
|
+
$query: DBClient["query"];
|
|
3557
|
+
$insert: DBClient["insert"];
|
|
3558
|
+
$update: DBClient["update"];
|
|
3559
|
+
$delete: DBClient["delete"];
|
|
3560
|
+
$findById: DBClient["findById"];
|
|
3561
|
+
$findMany: DBClient["findMany"];
|
|
3562
|
+
$put: DBClient["put"];
|
|
3563
|
+
$updateMany: DBClient["updateMany"];
|
|
3564
|
+
$deleteMany: DBClient["deleteMany"];
|
|
3565
|
+
$count: DBClient["count"];
|
|
3566
|
+
$search: DBClient["search"];
|
|
3567
|
+
$similar: RecoOps["similar"];
|
|
3568
|
+
$recommend: RecoOps["recommend"];
|
|
3569
|
+
$facets: DBClient["facets"];
|
|
3570
|
+
$supersede: DBClient["supersede"];
|
|
3571
|
+
$claim: DBClient["claim"];
|
|
3572
|
+
$lockRows: DBClient["lockRows"];
|
|
3573
|
+
$advisoryXactLock: DBClient["advisoryXactLock"];
|
|
3574
|
+
$attempt: DBClient["attempt"];
|
|
3575
|
+
/** Bkz. {@link EnvTypedDatabase.$transaction}. */
|
|
3576
|
+
/**
|
|
3577
|
+
* `opts.retry` verilirse, `SerializationFailure` / `DeadlockDetected`
|
|
3578
|
+
* alındığında plan BAŞTAN kurulup yeniden çalıştırılır (FR-037).
|
|
3579
|
+
* Varsayılan 0 — sessiz bir retry, idempotent olmayan bir işlemi çağıranın
|
|
3580
|
+
* haberi olmadan iki kez çalıştırırdı.
|
|
3581
|
+
*/
|
|
3582
|
+
$transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T, opts?: {
|
|
3583
|
+
retry?: number;
|
|
3584
|
+
}): Promise<Materialized<T>>;
|
|
3585
|
+
};
|
|
3586
|
+
/**
|
|
3587
|
+
* The typed-by-default Database surface: the raw string-keyed `DBClient` ops
|
|
3588
|
+
* PLUS a `tables` map typed against the project's generated `palbase-env.d.ts`,
|
|
3589
|
+
* a `transaction` that runs a whole plan in one request, and `asService()` for
|
|
3590
|
+
* the explicit RLS-bypass sibling.
|
|
3591
|
+
*
|
|
3592
|
+
* The low-level `txPlan` op is deliberately NOT re-exposed here: `transaction`
|
|
3593
|
+
* is the surface, and a hand-built plan would bypass the ref/guard machinery
|
|
3594
|
+
* that makes one safe to write.
|
|
3595
|
+
*/
|
|
3596
|
+
type EnvSchemaSurface = {
|
|
3597
|
+
public: EnvTables;
|
|
3598
|
+
} & {
|
|
3599
|
+
[S in keyof Schemas]: {
|
|
3600
|
+
[T in keyof Schemas[S]]: EnvTypedTable<Extract<Schemas[S][T], TableTypes>>;
|
|
3601
|
+
};
|
|
3602
|
+
};
|
|
3603
|
+
/**
|
|
3604
|
+
* Projenin veri yüzeyi: `Database.<şema>.<tablo>` + `$` önekli sistem üyeleri.
|
|
3605
|
+
*
|
|
3606
|
+
* Bugünkü `tables` ve `schema("x")` ikilisinin yerine geçer (FR-001). İki yol
|
|
3607
|
+
* tek yola iner: `public` de diğer şemalar gibi adıyla anılır, ara katman yoktur.
|
|
3608
|
+
*
|
|
3609
|
+
* @example
|
|
3610
|
+
* await Database.public.notes.findMany({ where: { owner: uid } });
|
|
3611
|
+
* await Database.billing.invoices.findMany({ where: { paid: false } });
|
|
3612
|
+
* await Database.$transaction((tx) => { … });
|
|
3613
|
+
*/
|
|
3614
|
+
type EnvTypedDatabase = EnvSchemaSurface & {
|
|
3615
|
+
/** Ham SQL. Generic verilmezse `unknown[]` döner (FR-021). */
|
|
3616
|
+
$query: DBClient["query"];
|
|
3617
|
+
$insert: DBClient["insert"];
|
|
3618
|
+
$update: DBClient["update"];
|
|
3619
|
+
$delete: DBClient["delete"];
|
|
3620
|
+
$findById: DBClient["findById"];
|
|
3621
|
+
$findMany: DBClient["findMany"];
|
|
3622
|
+
$put: DBClient["put"];
|
|
3623
|
+
$updateMany: DBClient["updateMany"];
|
|
3624
|
+
$deleteMany: DBClient["deleteMany"];
|
|
3625
|
+
$count: DBClient["count"];
|
|
3626
|
+
$search: DBClient["search"];
|
|
3627
|
+
$similar: RecoOps["similar"];
|
|
3628
|
+
$recommend: RecoOps["recommend"];
|
|
3629
|
+
$facets: DBClient["facets"];
|
|
3630
|
+
$supersede: DBClient["supersede"];
|
|
3631
|
+
$claim: DBClient["claim"];
|
|
3632
|
+
$lockRows: DBClient["lockRows"];
|
|
3633
|
+
$advisoryXactLock: DBClient["advisoryXactLock"];
|
|
3634
|
+
$attempt: DBClient["attempt"];
|
|
3635
|
+
/** RLS'i bypass eden kardeş yüzey. Kendisi `$asService` TAŞIMAZ — çift bypass yok. */
|
|
3636
|
+
$asService(): EnvServiceDatabase;
|
|
3637
|
+
/**
|
|
3638
|
+
* Bir transaction çalıştırır. Callback işlemleri TARİF eder; tarifin tamamı
|
|
3639
|
+
* tek istekte gider ve broker onu tek bir transaction içinde koşturur.
|
|
3640
|
+
* Callback SENKRONDUR: döndüğünde hiçbir şey çalışmamıştır.
|
|
3641
|
+
*/
|
|
3642
|
+
/**
|
|
3643
|
+
* `opts.retry` verilirse, `SerializationFailure` / `DeadlockDetected`
|
|
3644
|
+
* alındığında plan BAŞTAN kurulup yeniden çalıştırılır (FR-037).
|
|
3645
|
+
* Varsayılan 0 — sessiz bir retry, idempotent olmayan bir işlemi çağıranın
|
|
3646
|
+
* haberi olmadan iki kez çalıştırırdı.
|
|
3647
|
+
*/
|
|
3648
|
+
$transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T, opts?: {
|
|
3649
|
+
retry?: number;
|
|
3650
|
+
}): Promise<Materialized<T>>;
|
|
3651
|
+
};
|
|
3652
|
+
|
|
3653
|
+
/**
|
|
3654
|
+
* tx-plan.ts — `Database.$transaction()` as a PLAN, not a pinned session.
|
|
3655
|
+
*
|
|
3656
|
+
* A transaction used to be a conversation: BEGIN, then one network round trip
|
|
3657
|
+
* per operation, then COMMIT. Each of those round trips cost ~4 ms and, because
|
|
3658
|
+
* the pooler runs in transaction mode, an open transaction pinned a Postgres
|
|
3659
|
+
* backend for the whole conversation. A 121-operation statement upload pinned
|
|
3660
|
+
* one backend for ~490 ms.
|
|
3661
|
+
*
|
|
3662
|
+
* So the callback no longer TALKS to the database. It DESCRIBES what should
|
|
3663
|
+
* happen; the description is serialised and sent once; the broker runs the whole
|
|
3664
|
+
* thing inside one transaction and answers once. Committing on return and
|
|
3665
|
+
* rolling back on throw is unchanged — that is the only property tenant code
|
|
3666
|
+
* actually asked for.
|
|
3667
|
+
*
|
|
3668
|
+
* The consequences, stated plainly, because they are the whole design:
|
|
3669
|
+
*
|
|
3670
|
+
* - The callback is SYNCHRONOUS. There is nothing to await: no statement has
|
|
3671
|
+
* run yet when it returns. `async` on the callback and `await` inside it are
|
|
3672
|
+
* both compile errors (see {@link TxPlan} and {@link NotAwaitable}).
|
|
3673
|
+
* - `insert()` does not hand back a row, it hands back {@link TxRows}. Reading
|
|
3674
|
+
* a field requires `.expectOne(err)` first, which makes "what if the row
|
|
3675
|
+
* isn't there" a question you cannot route around: it is the argument.
|
|
3676
|
+
* - A field read from a row is a {@link Ref} — a PROMISE OF A VALUE THE SERVER
|
|
3677
|
+
* WILL PRODUCE, not the value. It can be written into a later operation and
|
|
3678
|
+
* it can be returned from the callback (it is substituted for the real value
|
|
3679
|
+
* before `transaction()` resolves). It cannot be branched on. See the
|
|
3680
|
+
* "Truthiness" note below — this is the sharp edge of the whole design.
|
|
3681
|
+
* - Control flow that needs a real value must move OUT of the callback: read
|
|
3682
|
+
* before the transaction, or express the condition as a guard
|
|
3683
|
+
* (`updateWhere({ id, accepted_at: null }, …).expectOne(new Conflict(…))`)
|
|
3684
|
+
* which the server evaluates and which rolls the whole plan back.
|
|
3685
|
+
*
|
|
3686
|
+
* # Truthiness — the hole this file CANNOT close
|
|
3687
|
+
*
|
|
3688
|
+
* JavaScript does not let a Proxy trap truthiness. `if (ref)` takes the true
|
|
3689
|
+
* branch, always, for every Ref, and no `get` handler ever runs. `tsc` is silent
|
|
3690
|
+
* because a Ref is a perfectly good object. So:
|
|
3691
|
+
*
|
|
3692
|
+
* const pot = tx.tables.pots.select({ id }, { limit: 1 }).expectOne(e);
|
|
3693
|
+
* if (!pot.balance) { … } // ← ALWAYS false. Silently wrong data.
|
|
3694
|
+
*
|
|
3695
|
+
* What this file does close: coercion (`Symbol.toPrimitive`/`valueOf`/
|
|
3696
|
+
* `toString`), awaiting (`then` is a callable member with a non-thenable
|
|
3697
|
+
* signature, which is a *compile* error), serialisation (`toJSON`), and nesting
|
|
3698
|
+
* a Ref inside a literal value where the server would store it as data. What it
|
|
3699
|
+
* cannot close is a bare truthiness test. The real defence is the build-time
|
|
3700
|
+
* static analysis (`tx_analysis.js`, phase P4); until that ships, this hole is
|
|
3701
|
+
* open and this comment is the only warning.
|
|
3702
|
+
*
|
|
3703
|
+
* # Wire contract
|
|
3704
|
+
*
|
|
3705
|
+
* The JSON this file emits is consumed by
|
|
3706
|
+
* the plan executor in `engine/db.ts`. That executor rejects
|
|
3707
|
+
* unknown fields at every level, so an op carries EXACTLY the fields its kind
|
|
3708
|
+
* takes. Everything here that looks like a needless restriction is one of the
|
|
3709
|
+
* server's rules made visible early:
|
|
3710
|
+
*
|
|
3711
|
+
* - `$ref` only points BACKWARDS, and only at an op statically known to yield
|
|
3712
|
+
* at most one row (insert, or a `one`/`atMost 1` guard, or `select limit 1`).
|
|
3713
|
+
* `.expectOne()` is what this file uses to satisfy that, always.
|
|
3714
|
+
* - `$expr` is a closed set: `inc`/`dec` (update only — they read the column's
|
|
3715
|
+
* current value) and `now()`.
|
|
3716
|
+
* - `update`/`delete` require a `where`; `insert` refuses one.
|
|
3717
|
+
* - `insertMany` rows must all set the same columns.
|
|
3718
|
+
* - ≤1000 ops, ≤5000 rows per insertMany, ≤8 MiB of JSON.
|
|
3719
|
+
*
|
|
3720
|
+
* Column keys are emitted SORTED, so the same callback always produces byte-
|
|
3721
|
+
* identical JSON. That is what lets the Go decoder be locked to golden files
|
|
3722
|
+
* this SDK emits (`testdata/tx_plan_golden/`).
|
|
3723
|
+
*/
|
|
3724
|
+
/**
|
|
3725
|
+
* A plan handle was used as if it were a value: awaited, coerced to a string or
|
|
3726
|
+
* number, serialised, or nested inside another value.
|
|
3727
|
+
*
|
|
3728
|
+
* Thrown while the callback is still BUILDING the plan, so nothing has been sent
|
|
3729
|
+
* and nothing has been written.
|
|
3730
|
+
*/
|
|
3731
|
+
|
|
3732
|
+
declare class TxRefError extends Error {
|
|
3733
|
+
constructor(message: string);
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* The plan the callback described cannot be sent: it breaks a rule the server
|
|
3737
|
+
* would reject, and rejecting it here names the line that wrote it instead of
|
|
3738
|
+
* returning a 400 about an op index.
|
|
3739
|
+
*/
|
|
3740
|
+
declare class TxPlanError extends Error {
|
|
3741
|
+
constructor(message: string);
|
|
3742
|
+
}
|
|
3743
|
+
/** A backwards reference to an earlier op's single-row result. */
|
|
3744
|
+
interface TxWireRef {
|
|
3745
|
+
$ref: {
|
|
3746
|
+
op: number;
|
|
3747
|
+
field: string;
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
/** A call from the server's closed function set. */
|
|
3751
|
+
interface TxWireExpr {
|
|
3752
|
+
$expr: {
|
|
3753
|
+
fn: "inc" | "dec";
|
|
3754
|
+
by: number | string;
|
|
3755
|
+
} | {
|
|
3756
|
+
fn: "now";
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
/** One value in a `values`/`set`/`where` map: a literal, a `$ref` or a `$expr`. */
|
|
3760
|
+
type TxWireValue = TxWireRef | TxWireExpr | unknown;
|
|
3761
|
+
/** The tenant's declared expectation about an op's row count. `slot` indexes the
|
|
3762
|
+
* client-side error table — the error OBJECT never travels. */
|
|
3763
|
+
interface TxWireGuard {
|
|
3764
|
+
kind: "one" | "none" | "atLeast" | "atMost";
|
|
3765
|
+
n: number;
|
|
3766
|
+
slot: number;
|
|
3767
|
+
}
|
|
3768
|
+
/** One operation in the wire plan. Fields are omitted, never null: the decoder
|
|
3769
|
+
* rejects a field that does not belong to the op's kind. */
|
|
3770
|
+
interface TxWireOp {
|
|
3771
|
+
/** upsert and insertMany: the columns Postgres matches on. */
|
|
3772
|
+
onConflict?: readonly string[];
|
|
3773
|
+
/** insertMany only: what a collision does. Absent means no ON CONFLICT clause
|
|
3774
|
+
* at all, which is what every insertMany did before this option existed. */
|
|
3775
|
+
action?: "ignore" | "update";
|
|
3776
|
+
op: "insert" | "insertMany" | "upsert" | "update" | "delete" | "select";
|
|
3777
|
+
table: string;
|
|
3778
|
+
values?: Record<string, TxWireValue>;
|
|
3779
|
+
rows?: Record<string, TxWireValue>[];
|
|
3780
|
+
set?: Record<string, TxWireValue>;
|
|
3781
|
+
where?: Record<string, TxWireValue>;
|
|
3782
|
+
limit?: number;
|
|
3783
|
+
lock?: "update";
|
|
3784
|
+
guard?: TxWireGuard;
|
|
3785
|
+
}
|
|
3786
|
+
/** The plan the engine executes on the request's own transaction. */
|
|
3787
|
+
interface TxPlanBody {
|
|
3788
|
+
ops: TxWireOp[];
|
|
3789
|
+
}
|
|
3790
|
+
/** One op's outcome, positionally matched to the plan's ops. */
|
|
3791
|
+
interface TxPlanOpResult {
|
|
3792
|
+
rows: Record<string, unknown>[];
|
|
3793
|
+
rows_affected: number;
|
|
3794
|
+
}
|
|
3795
|
+
/** One result per op, in plan order. */
|
|
3796
|
+
interface TxPlanResponse {
|
|
3797
|
+
results: TxPlanOpResult[];
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* The fields the runtime must copy from the broker's error envelope onto the
|
|
3801
|
+
* rejection it throws out of {@link DBClient.txPlan}.
|
|
3802
|
+
*
|
|
3803
|
+
* `slot` is the whole point: on a guard failure the server answers with the
|
|
3804
|
+
* INDEX of the expectation that did not hold, never with an error message of its
|
|
3805
|
+
* own, and this SDK maps that index back to the `Error` the callback handed to
|
|
3806
|
+
* `.expectOne(…)`. Without `slot` a guard failure degrades to a generic 409.
|
|
3807
|
+
*/
|
|
3808
|
+
interface TxPlanRejection {
|
|
3809
|
+
status?: number;
|
|
3810
|
+
/** `tx_plan_invalid` | `tx_guard_failed` | `tx_ref_unresolved` | a pg class. */
|
|
3811
|
+
error_code?: string;
|
|
3812
|
+
/** Present only for `tx_guard_failed`: the client-side error table index. */
|
|
3813
|
+
slot?: number;
|
|
3814
|
+
/** Present on a database error: which op failed. */
|
|
3815
|
+
op?: number;
|
|
3816
|
+
}
|
|
3817
|
+
declare const refBrand: unique symbol;
|
|
3818
|
+
declare const rowBrand: unique symbol;
|
|
3819
|
+
declare const rowsBrand: unique symbol;
|
|
3820
|
+
/**
|
|
3821
|
+
* Makes a handle a compile error to `await`.
|
|
3822
|
+
*
|
|
3823
|
+
* `then` is declared as a CALLABLE member whose signature is not `PromiseLike`,
|
|
3824
|
+
* which is precisely the shape TypeScript rejects: `await handle` is TS1320 and
|
|
3825
|
+
* `async () => handle` is TS1058. A non-callable `then` would not do it — the
|
|
3826
|
+
* compiler simply ignores those.
|
|
3827
|
+
*/
|
|
3828
|
+
interface NotAwaitable {
|
|
3829
|
+
/** Not a promise. Nothing here has run yet; there is nothing to await. */
|
|
3830
|
+
then(doNotAwaitAPlanHandle: "a transaction plan is built synchronously"): never;
|
|
3831
|
+
}
|
|
3832
|
+
/**
|
|
3833
|
+
* A value the SERVER will produce, standing in for a column of a row this plan
|
|
3834
|
+
* writes or reads.
|
|
3835
|
+
*
|
|
3836
|
+
* Legal uses: write it into a later operation's `values`/`set`/`where`, or
|
|
3837
|
+
* return it from the callback (it is replaced by the real value before
|
|
3838
|
+
* `transaction()` resolves).
|
|
3839
|
+
*
|
|
3840
|
+
* Illegal, and caught: `await`, `String(ref)`, `` `${ref}` ``, `ref + 1`,
|
|
3841
|
+
* `JSON.stringify(ref)`, burying it inside a jsonb object.
|
|
3842
|
+
*
|
|
3843
|
+
* Illegal, and NOT caught: `if (ref)`. See the truthiness note at the top.
|
|
3844
|
+
*/
|
|
3845
|
+
interface Ref<T> extends NotAwaitable {
|
|
3846
|
+
readonly [refBrand]: T;
|
|
3847
|
+
}
|
|
3848
|
+
/** The brand carried by a single-row handle, and the seam `Materialized` reads
|
|
3849
|
+
* to turn `return st` into the whole row. */
|
|
3850
|
+
interface TxRowHandle<Row> extends NotAwaitable {
|
|
3851
|
+
readonly [rowBrand]: Row;
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* A row this plan is known to produce exactly one of. Every property is a
|
|
3855
|
+
* {@link Ref}; returning the handle itself yields the whole row.
|
|
3856
|
+
*
|
|
3857
|
+
* Only `.expectOne(err)` produces one — which is the design: a row you can read
|
|
3858
|
+
* fields from is a row whose absence you have already answered for.
|
|
3859
|
+
*/
|
|
3860
|
+
type TxRow<Row> = {
|
|
3861
|
+
readonly [K in keyof Row]: Ref<Row[K]>;
|
|
3862
|
+
} & TxRowHandle<Row>;
|
|
3863
|
+
/**
|
|
3864
|
+
* The result of one operation, before any expectation is declared about it.
|
|
3865
|
+
*
|
|
3866
|
+
* Deliberately not a row and not a list: an operation's row count is not known
|
|
3867
|
+
* until the server runs it, so the only thing that can be said about it here is
|
|
3868
|
+
* an EXPECTATION. Declaring one is also the only way to get a readable row.
|
|
3869
|
+
*
|
|
3870
|
+
* At most one expectation per operation — the wire carries one guard per op, and
|
|
3871
|
+
* a second call throws rather than silently dropping the first.
|
|
3872
|
+
*/
|
|
3873
|
+
interface TxRows<Row> extends NotAwaitable {
|
|
3874
|
+
readonly [rowsBrand]: Row;
|
|
3875
|
+
/**
|
|
3876
|
+
* Require exactly one row, and read it. On any other count the server rolls
|
|
3877
|
+
* the whole transaction back and this `error` is thrown to the caller.
|
|
3878
|
+
*
|
|
3879
|
+
* This is the only way to reach a row's fields, and the only shape a `$ref`
|
|
3880
|
+
* may point at.
|
|
3881
|
+
*/
|
|
3882
|
+
expectOne(error: Error): TxRow<Row>;
|
|
3883
|
+
/** Require zero rows (e.g. "this membership must not already exist"). */
|
|
3884
|
+
expectNone(error: Error): void;
|
|
3885
|
+
/** Require at least `n` rows. */
|
|
3886
|
+
expectAtLeast(n: number, error: Error): void;
|
|
3887
|
+
/** Require at most `n` rows. */
|
|
3888
|
+
expectAtMost(n: number, error: Error): void;
|
|
3889
|
+
}
|
|
3890
|
+
/** `now()` — the server's clock, usable wherever a value is. */
|
|
3891
|
+
interface TxNow extends NotAwaitable {
|
|
3892
|
+
readonly $expr: {
|
|
3893
|
+
fn: "now";
|
|
3894
|
+
};
|
|
3895
|
+
}
|
|
3896
|
+
/** `inc(n)` / `dec(n)` — read the column's CURRENT value and write it back
|
|
3897
|
+
* changed. Only meaningful in an update's `set`, which is where the types allow
|
|
3898
|
+
* it and where the server allows it. */
|
|
3899
|
+
interface TxColumnExpr extends NotAwaitable {
|
|
3900
|
+
readonly $expr: {
|
|
3901
|
+
fn: "inc" | "dec";
|
|
3902
|
+
by: number | string;
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Resolve a callback's return type against what actually comes back: every
|
|
3907
|
+
* {@link Ref} becomes its value, every {@link TxRow} becomes its row, and
|
|
3908
|
+
* anything else keeps its shape.
|
|
3909
|
+
*
|
|
3910
|
+
* A {@link TxRows} resolves to an explanatory string type rather than a row
|
|
3911
|
+
* list: it has no single answer to give, and saying so in the type is louder
|
|
3912
|
+
* than a runtime throw.
|
|
3913
|
+
*/
|
|
3914
|
+
type Materialized<T> = T extends Ref<infer U> ? U : T extends TxRowHandle<infer R> ? R : T extends TxRows<unknown> ? "a TxRows cannot leave the transaction callback — read a row with .expectOne(err) first" : T extends Date ? T : T extends object ? {
|
|
3915
|
+
[K in keyof T]: Materialized<T[K]>;
|
|
3916
|
+
} : T;
|
|
3917
|
+
/** A value written by an INSERT: a literal, an earlier row's field, or `now()`.
|
|
3918
|
+
* `inc`/`dec` are absent on purpose — they read a current value, and an inserted
|
|
3919
|
+
* row has none. */
|
|
3920
|
+
type TxInsertValue<V> = V | Ref<V> | TxNow;
|
|
3921
|
+
/** A value written by an UPDATE's `set`: everything an insert takes, plus the
|
|
3922
|
+
* read-modify-write expressions. */
|
|
3923
|
+
/**
|
|
3924
|
+
* TEK KURAL: ifade tutamağı yalnız sayısal-benzeri kolonlarda.
|
|
3925
|
+
*
|
|
3926
|
+
* Bu tip KOŞULSUZDU ve doğrudan yolun `SetValue<V>`'si koşulluydu, yani aynı
|
|
3927
|
+
* nesne için İKİ tip kuralı vardı: `tx.tables.todos.updateWhere({id}, { done:
|
|
3928
|
+
* increment(1) })` (boolean kolon!) DERLENİYOR, `updateMany`'nin aynısı derleme
|
|
3929
|
+
* hatası veriyordu. Bu run'ın kapatmak için var olduğu şey "aynı iş için iki
|
|
3930
|
+
* uyumsuz yazım"dı; tip kuralı ikinci yazımın kendisi olmuştu (gözcü I6/I-1).
|
|
3931
|
+
*/
|
|
3932
|
+
type TxSetValue<V> = V | Ref<V> | TxNow | (NonNullable<V> extends number | string ? TxColumnExpr : never);
|
|
3933
|
+
/** An insert payload: the table's insert shape, with refs and `now()` allowed. */
|
|
3934
|
+
type TxInsertShape<Insert> = {
|
|
3935
|
+
[K in keyof Insert]: TxInsertValue<Insert[K]>;
|
|
3936
|
+
};
|
|
3937
|
+
/** An update's `set`: any subset of the insert shape, with expressions allowed. */
|
|
3938
|
+
type TxSetShape<Insert> = {
|
|
3939
|
+
[K in keyof Insert]?: TxSetValue<Insert[K]>;
|
|
3940
|
+
};
|
|
3941
|
+
/**
|
|
3942
|
+
* A filter. Every entry is an equality test and they are AND-ed; a `null`
|
|
3943
|
+
* becomes `IS NULL`, which is what makes `{ accepted_at: null }` a usable
|
|
3944
|
+
* "not yet accepted" guard rather than a clause that matches nothing.
|
|
3945
|
+
*/
|
|
3946
|
+
/**
|
|
3947
|
+
* Plan filtresinin tipi — `WhereFilter<Row>` ile AYNI sözlük, artı `Ref`.
|
|
3948
|
+
*
|
|
3949
|
+
* Eskiden yalnız eşitlikti (`Row[K] | Ref<Row[K]>`), ve iki şeye mal oluyordu:
|
|
3950
|
+
* FR-014'ün amiral deseni (`{ balance: { gte: amount } }`) `$transaction`
|
|
3951
|
+
* İÇİNDE yazılamıyordu — koşullu bir yazmayı plana koyamayan yazar `$query`'ye
|
|
3952
|
+
* düşüyordu — ve motor tarafında tip atlandığında aynı nesne SESSİZCE parametre
|
|
3953
|
+
* olarak bağlanıyordu.
|
|
3954
|
+
*
|
|
3955
|
+
* `Ref` fazladan üye ve öyle kalmalı: bir plan filtresi ÖNCEKİ bir işlemin
|
|
3956
|
+
* döndürdüğü değere bakabilir, `findMany` bakamaz — plan dışında böyle bir
|
|
3957
|
+
* "önceki işlem" yok.
|
|
3958
|
+
*/
|
|
3959
|
+
type TxWhereField<Row, K extends keyof Row> = WhereOpWith<Row[K], ColRefOf<Row, Row[K]> | Ref<Row[K]>>;
|
|
3960
|
+
type TxWhere<Row, Rels = unknown> = {
|
|
3961
|
+
[K in keyof Row]?: TxWhereField<Row, K>;
|
|
3962
|
+
} & {
|
|
3963
|
+
OR?: TxWhere<Row, Rels>[];
|
|
3964
|
+
AND?: TxWhere<Row, Rels>[];
|
|
3965
|
+
NOT?: TxWhere<Row, Rels>;
|
|
3966
|
+
} & HasOnly<Rels>;
|
|
3967
|
+
/** Options for a plan `select`. */
|
|
3968
|
+
interface TxSelectOptions {
|
|
3969
|
+
/** Cap the rows read. */
|
|
3970
|
+
limit?: number;
|
|
3971
|
+
/** Take a real `FOR UPDATE` row lock for the rest of the transaction. */
|
|
3972
|
+
lock?: "update";
|
|
3973
|
+
}
|
|
3974
|
+
/** One table, as the plan sees it. */
|
|
3975
|
+
interface TxTable<Row, Insert, Rels = unknown> {
|
|
3976
|
+
/** Insert one row. Returns a handle — call `.expectOne(err)` to read fields. */
|
|
3977
|
+
insert(values: TxInsertShape<Insert>): TxRows<Row>;
|
|
3978
|
+
/**
|
|
3979
|
+
* Insert many rows in ONE statement. Every row must set the same columns
|
|
3980
|
+
* (a row that omits one would silently take the column's default).
|
|
3981
|
+
*
|
|
3982
|
+
* An empty list writes nothing and sends nothing.
|
|
3983
|
+
*/
|
|
3984
|
+
/**
|
|
3985
|
+
* Insert many rows in ONE statement, optionally choosing what a collision does.
|
|
3986
|
+
*
|
|
3987
|
+
* Without `opts` this is a plain multi-row INSERT and a collision aborts the
|
|
3988
|
+
* transaction — the behaviour every call had before the option existed.
|
|
3989
|
+
*
|
|
3990
|
+
* `action: "ignore"` emits `ON CONFLICT DO NOTHING`, which is how "insert the
|
|
3991
|
+
* ones that are new" becomes one round-trip instead of one per row with a
|
|
3992
|
+
* 23505 caught around each. **The returned rows are the ones actually
|
|
3993
|
+
* INSERTED**: a row that collided is skipped, so it is absent from the result
|
|
3994
|
+
* — Postgres does not return what it did not write.
|
|
3995
|
+
*
|
|
3996
|
+
* `action: "update"` emits `ON CONFLICT DO UPDATE`, setting every non-conflict
|
|
3997
|
+
* column from the incoming row, and every row comes back.
|
|
3998
|
+
*/
|
|
3999
|
+
insertMany(rows: readonly TxInsertShape<Insert>[], opts?: {
|
|
4000
|
+
onConflict: readonly Extract<keyof Row, string>[];
|
|
4001
|
+
action?: "ignore" | "update";
|
|
4002
|
+
}): TxRows<Row>;
|
|
4003
|
+
/**
|
|
4004
|
+
* Satırı yaz, `onConflict` kolonlarında çakışırsa üzerine yaz — planın
|
|
4005
|
+
* savepoint'i içinde, `Database.<şema>.<tablo>.put()` ile AYNI anlamda.
|
|
4006
|
+
*
|
|
4007
|
+
* Adı bilerek aynı: aynı iş için transaction içinde ve dışında iki farklı
|
|
4008
|
+
* yazım, bu run'ın kapatmak için var olduğu şeydir (P1). TEL şekli
|
|
4009
|
+
* (`op: "upsert"`) değişmedi — o iç sözleşme, yazarın gördüğü ad değil.
|
|
4010
|
+
*
|
|
4011
|
+
* Bir operasyon olmasının sebebi: alternatifi burada yazılamaz — başarısız
|
|
4012
|
+
* bir insert tüm transaction'ı abort eder, yani "dene, sonra geri düş" iki
|
|
4013
|
+
* plan adımı olamaz.
|
|
4014
|
+
*/
|
|
4015
|
+
put(values: TxInsertShape<Insert>, options: {
|
|
4016
|
+
onConflict: readonly Extract<keyof Row, string>[];
|
|
4017
|
+
}): TxRows<Row>;
|
|
4018
|
+
/**
|
|
4019
|
+
* Update every row matching `where`. The filter comes first because it is the
|
|
4020
|
+
* dangerous half: an update whose `where` you got wrong rewrites rows you
|
|
4021
|
+
* never looked at. The server refuses an update with no `where` at all.
|
|
4022
|
+
*/
|
|
4023
|
+
updateWhere(where: TxWhere<Row, Rels>, set: TxSetShape<Insert>): TxRows<Row>;
|
|
4024
|
+
/** Delete every row matching `where`. The server refuses an unfiltered delete. */
|
|
4025
|
+
deleteWhere(where: TxWhere<Row, Rels>): TxRows<Row>;
|
|
4026
|
+
/** Read rows, optionally locking them for the rest of the transaction. */
|
|
4027
|
+
select(where?: TxWhere<Row, Rels>, options?: TxSelectOptions): TxRows<Row>;
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* The handle a transaction callback receives.
|
|
4031
|
+
*
|
|
4032
|
+
* It carries tables and nothing else: no `query`, no `findById`, no `asService`.
|
|
4033
|
+
* A read whose value the plan does not write belongs OUTSIDE the transaction,
|
|
4034
|
+
* where it costs one round trip and can be branched on like an ordinary value.
|
|
4035
|
+
*/
|
|
4036
|
+
interface TxPlanHandle<TTables> {
|
|
4037
|
+
/**
|
|
4038
|
+
* @deprecated `tx.public` kullanın. Bu ad public'in takma adı olarak DURUYOR
|
|
4039
|
+
* (göç notu onu öğretiyor ve her mevcut çağrı onu kullanıyor), ama ARTIK
|
|
4040
|
+
* ÖĞRETİLMİYOR: doğrudan yüzeyde `Database.tables` FR-001 ile kaldırıldı, ve
|
|
4041
|
+
* plan yüzeyinin onu öğretmeye devam etmesi yazarı bir yüzeyde çalışıp
|
|
4042
|
+
* diğerinde derlenmeyen bir yazıma alıştırıyordu (gözcü M-6).
|
|
4043
|
+
*/
|
|
4044
|
+
tables: TTables;
|
|
4045
|
+
}
|
|
4046
|
+
/** The server's `now()`. */
|
|
4047
|
+
declare function now(): TxNow;
|
|
4048
|
+
/** Add `by` to the column's current value. Only valid in an update's `set`. */
|
|
4049
|
+
declare function increment(by: number | string): TxColumnExpr;
|
|
4050
|
+
/**
|
|
4051
|
+
* `increment`'in eski adı. AYNI fabrikadır — iki uygulama değil, iki ad.
|
|
4052
|
+
*
|
|
4053
|
+
* @deprecated `increment()` kullanın; bu ad geriye dönük uyumluluk için duruyor.
|
|
4054
|
+
*/
|
|
4055
|
+
declare const inc: typeof increment;
|
|
4056
|
+
/** Subtract `by` from the column's current value. Only valid in an update's `set`. */
|
|
4057
|
+
declare function decrement(by: number | string): TxColumnExpr;
|
|
4058
|
+
/**
|
|
4059
|
+
* `decrement`'in eski adı. AYNI fabrikadır.
|
|
4060
|
+
*
|
|
4061
|
+
* @deprecated `decrement()` kullanın.
|
|
4062
|
+
*/
|
|
4063
|
+
declare const dec: typeof decrement;
|
|
4064
|
+
|
|
4065
|
+
/**
|
|
4066
|
+
* The brand that identifies an HttpError ACROSS SDK instances.
|
|
4067
|
+
*
|
|
4068
|
+
* A process legitimately holds more than one copy of this SDK — the runtime
|
|
4069
|
+
* loads the engine from its own node_modules while the tenant's bundle carries
|
|
4070
|
+
* an inlined copy, which is why the controller registry and the error registry
|
|
4071
|
+
* are both anchored on `Symbol.for`. The one place that did not follow the
|
|
4072
|
+
* pattern was the engine's catch: `err instanceof HttpError` compares CLASS
|
|
4073
|
+
* IDENTITY, so a `throw new NotFound()` from the bundle's copy did not match
|
|
4074
|
+
* the engine's copy and every typed error in every deployed backend degraded to
|
|
4075
|
+
* `500 internal_error`. Measured through the edge on a real deploy: a route
|
|
4076
|
+
* throwing `NotFound` answered 500 while the runtime's own log printed the
|
|
4077
|
+
* error object with `status: 404` right beside it.
|
|
4078
|
+
*
|
|
4079
|
+
* `Symbol.for` puts this in the cross-realm registry, so every copy of the SDK
|
|
4080
|
+
* agrees on it by VALUE rather than by identity.
|
|
4081
|
+
*/
|
|
4082
|
+
declare const HTTP_ERROR_BRAND: unique symbol;
|
|
4083
|
+
/** HTTP error with structured error response format.
|
|
4084
|
+
*
|
|
4085
|
+
* The base class for the throwable error classes (`PalError`, `Conflict`,
|
|
4086
|
+
* `NotFound`, …). Construct one directly with `throw new HttpError(404,
|
|
4087
|
+
* "todo_not_found", "No such todo")`, or throw a named subclass
|
|
4088
|
+
* (`throw new NotFound("todo not found")`). The runtime catches any `HttpError`
|
|
4089
|
+
* and emits the standard envelope; on the wire (and to iOS) it surfaces as
|
|
4090
|
+
* `BackendError.server(code, status, message, requestId)`.
|
|
4091
|
+
*
|
|
4092
|
+
* The optional `data` field carries a structured payload alongside the
|
|
4093
|
+
* standard envelope — for errors that need to ship extra context
|
|
4094
|
+
* (e.g. `new Conflict("locked", "title_locked", { retryAfter: 30 })`). It rides
|
|
4095
|
+
* through to the iOS typed enum's associated value.
|
|
4096
|
+
*/
|
|
4097
|
+
declare class HttpError extends Error {
|
|
4098
|
+
readonly status: number;
|
|
4099
|
+
readonly error: string;
|
|
4100
|
+
readonly errorDescription: string;
|
|
4101
|
+
readonly data?: unknown;
|
|
4102
|
+
/** See {@link HTTP_ERROR_BRAND} — how the engine recognises this across SDK copies. */
|
|
4103
|
+
readonly [HTTP_ERROR_BRAND] = true;
|
|
4104
|
+
constructor(status: number, error: string, errorDescription: string, data?: unknown);
|
|
4105
|
+
/**
|
|
4106
|
+
* Serialize to the standard Palbase error response format.
|
|
4107
|
+
* The `requestId` is injected by the runtime layer from the request context.
|
|
4108
|
+
* When called without arguments (e.g. JSON.stringify), request_id is omitted.
|
|
4109
|
+
* When `data` is set, it is appended as a strict-superset field.
|
|
4110
|
+
*/
|
|
4111
|
+
toJSON(requestId?: string): {
|
|
4112
|
+
error: string;
|
|
4113
|
+
error_description: string;
|
|
4114
|
+
status: number;
|
|
4115
|
+
request_id?: string;
|
|
4116
|
+
data?: unknown;
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* Throw with a custom HTTP status + wire code. The general-purpose escape hatch
|
|
4121
|
+
* when none of the named classes (`Conflict`/`NotFound`/…) fits.
|
|
4122
|
+
*
|
|
4123
|
+
* @example
|
|
4124
|
+
* throw new PalError(418, "teapot", "I'm a teapot");
|
|
4125
|
+
*/
|
|
4126
|
+
declare class PalError extends HttpError {
|
|
4127
|
+
constructor(status: number, code: string, description: string, data?: unknown);
|
|
4128
|
+
}
|
|
4129
|
+
/** Base for the named status classes. Each subclass fixes its HTTP status; the
|
|
4130
|
+
* `code` defaults to the class's canonical wire code (overridable), and the
|
|
4131
|
+
* `message` defaults to a human-readable label (overridable). */
|
|
4132
|
+
declare abstract class NamedHttpError extends HttpError {
|
|
4133
|
+
protected constructor(status: number, defaultCode: string, name: string, message?: string, code?: string, data?: unknown);
|
|
4134
|
+
}
|
|
4135
|
+
/**
|
|
4136
|
+
* 400 — the request was malformed or failed validation. Carries a fixed typed
|
|
4137
|
+
* payload: `new BadRequest({ fields: [{ field: "email", message: "invalid" }] })`.
|
|
4138
|
+
* The shape is declared once in the SDK so codegen surfaces `error.data.fields`
|
|
4139
|
+
* typed on the client.
|
|
4140
|
+
*/
|
|
4141
|
+
declare class BadRequest extends NamedHttpError {
|
|
4142
|
+
readonly data: BadRequestData;
|
|
4143
|
+
constructor(data: BadRequestData, message?: string);
|
|
4144
|
+
}
|
|
4145
|
+
/** 401 — the caller is not authenticated. */
|
|
4146
|
+
declare class Unauthorized extends NamedHttpError {
|
|
4147
|
+
constructor(message?: string, code?: string, data?: unknown);
|
|
4148
|
+
}
|
|
4149
|
+
/** 403 — the caller is authenticated but not allowed. */
|
|
4150
|
+
declare class Forbidden extends NamedHttpError {
|
|
4151
|
+
constructor(message?: string, code?: string, data?: unknown);
|
|
4152
|
+
}
|
|
4153
|
+
/** 404 — the requested resource does not exist. */
|
|
4154
|
+
declare class NotFound extends NamedHttpError {
|
|
4155
|
+
constructor(message?: string, code?: string, data?: unknown);
|
|
4156
|
+
}
|
|
4157
|
+
/** 409 — the request conflicts with the current state. */
|
|
4158
|
+
declare class Conflict extends NamedHttpError {
|
|
4159
|
+
constructor(message?: string, code?: string, data?: unknown);
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* 409 — a write was refused because it would duplicate an existing row.
|
|
4163
|
+
* Carries the NAME of the unique constraint Postgres named (`users_email_key`).
|
|
4164
|
+
*
|
|
4165
|
+
* The engine produces it: a statement rejected with SQLSTATE `23505` is
|
|
4166
|
+
* converted here rather than surfacing as an opaque driver error (see
|
|
4167
|
+
* `engine/db.ts`, `diagnosingDriver`). What that removes is the string match —
|
|
4168
|
+
* before this, the only way to act on a duplicate was to test the driver
|
|
4169
|
+
* message for "duplicate key value violates unique constraint", a contract
|
|
4170
|
+
* nobody signed that breaks on a Postgres upgrade, a locale, or a constraint
|
|
4171
|
+
* rename, silently and in production.
|
|
4172
|
+
*
|
|
4173
|
+
* THE NAME IS A FIELD AND STAYS OUT OF THE DEFAULT MESSAGE. The two are not
|
|
4174
|
+
* the same audience. `constraint` is read by the code that catches this — the
|
|
4175
|
+
* developer, who already knows the schema. `errorDescription` is the HTTP
|
|
4176
|
+
* response body, and an UNCAUGHT duplicate puts it in front of the
|
|
4177
|
+
* application's end user: `users_email_key` there discloses how the schema is
|
|
4178
|
+
* built to whoever sent the request. The platform's own data API took the same
|
|
4179
|
+
* decision one surface over and wrote down why —
|
|
4180
|
+
* `v2/internal/modules/database/internal/handler/pgerror.go:83-87` collapses
|
|
4181
|
+
* every 23xxx to a generic conflict, "never disclose the constraint/column
|
|
4182
|
+
* name". A thrower who WANTS the name on the wire passes it deliberately
|
|
4183
|
+
* (`new UniqueViolation(c, \`\${c} already exists\`)`, or through `data`).
|
|
4184
|
+
*
|
|
4185
|
+
* @example
|
|
4186
|
+
* try {
|
|
4187
|
+
* await Database.public.users.insert({ email });
|
|
4188
|
+
* } catch (e) {
|
|
4189
|
+
* if (UniqueViolation.is(e) && e.constraint === "users_email_key") {
|
|
4190
|
+
* throw new Conflict("That email is taken", "email_taken");
|
|
4191
|
+
* }
|
|
4192
|
+
* throw e;
|
|
4193
|
+
* }
|
|
4194
|
+
*/
|
|
4195
|
+
declare class UniqueViolation extends Conflict {
|
|
4196
|
+
/**
|
|
4197
|
+
* Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
|
|
4198
|
+
* constructed it.
|
|
4199
|
+
*
|
|
4200
|
+
* Use this instead of `instanceof`. Measured on a live stack: a controller
|
|
4201
|
+
* bundle INLINES its own copy of `@palbase/backend`, and the engine that
|
|
4202
|
+
* raises this error is the runtime's copy. Two copies, two class identities,
|
|
4203
|
+
* and `e instanceof UniqueViolation` is false in the one place a caller
|
|
4204
|
+
* writes it — a check that reads as correct and silently never matches.
|
|
4205
|
+
*/
|
|
4206
|
+
static is(e: unknown): e is UniqueViolation;
|
|
4207
|
+
/** The unique constraint the statement violated, as Postgres named it.
|
|
4208
|
+
* `""` when the driver did not say which — see `engine/db.ts`. */
|
|
4209
|
+
readonly constraint: string;
|
|
4210
|
+
constructor(constraint: string, message?: string, code?: string, data?: unknown);
|
|
4211
|
+
}
|
|
4212
|
+
/**
|
|
4213
|
+
* Eşzamanlılık yüzünden BAŞARISIZ OLAN, ama TEKRAR DENENİRSE başarılı olabilecek
|
|
4214
|
+
* hataların ortak yüzeyi (FR-036).
|
|
4215
|
+
*
|
|
4216
|
+
* `retryable: true` bir tahmin değil, Postgres'in söylediği şey: `40001` ve
|
|
4217
|
+
* `40P01` "bu işlemi baştan çalıştır" anlamına gelir. Kümenin DAR tutulması
|
|
4218
|
+
* kasıtlı — `23505` buraya girseydi, tekrar denemek aynı cevabı verir ve
|
|
4219
|
+
* döngü sonsuza kadar dönerdi.
|
|
4220
|
+
*/
|
|
4221
|
+
interface RetryableError extends Error {
|
|
4222
|
+
readonly retryable: true;
|
|
4223
|
+
/** Beş karakterlik SQLSTATE — hangi eşzamanlılık hatası olduğunu SÖYLER. */
|
|
4224
|
+
readonly sqlstate: string;
|
|
4225
|
+
}
|
|
4226
|
+
/**
|
|
4227
|
+
* `e` tekrar denenebilir mi?
|
|
4228
|
+
*
|
|
4229
|
+
* `instanceof` DEĞİL şekil kontrolü — ve sebebi ÖLÇÜLMÜŞ (bkz. {@link
|
|
4230
|
+
* UniqueViolation.is}): bir controller bundle'ı kendi `@palbase/backend`
|
|
4231
|
+
* kopyasını inline'lıyor, hatayı fırlatan motor ise runtime'ın kopyası. İki
|
|
4232
|
+
* sınıf kimliği, ve `e instanceof SerializationFailure` yazan tek yerde
|
|
4233
|
+
* sessizce hiç eşleşmiyor.
|
|
4234
|
+
*/
|
|
4235
|
+
declare function isRetryable(e: unknown): e is RetryableError;
|
|
4236
|
+
/**
|
|
4237
|
+
* `40001 serialization_failure` — SERIALIZABLE ya da REPEATABLE READ altında
|
|
4238
|
+
* eşzamanlı bir işlem bu işlemi geçersiz kıldı.
|
|
4239
|
+
*
|
|
4240
|
+
* D-016: bu hata BLOKLANMIYOR, FIRLATILIYOR. Retry'sız bir izolasyon
|
|
4241
|
+
* yükseltmesi sunmak defect'tir — kullanıcıya çalışmayan bir düğme vermektir:
|
|
4242
|
+
* SERIALIZABLE'a çıkıp `40001`'i yakalamayan kod, önceden çalışan işlemleri
|
|
4243
|
+
* rastgele kaybetmeye başlar.
|
|
4244
|
+
*/
|
|
4245
|
+
declare class SerializationFailure extends Conflict implements RetryableError {
|
|
4246
|
+
readonly retryable: true;
|
|
4247
|
+
readonly sqlstate: string;
|
|
4248
|
+
static is(e: unknown): e is SerializationFailure;
|
|
4249
|
+
constructor(sqlstate?: string, message?: string, data?: unknown);
|
|
4250
|
+
}
|
|
4251
|
+
/**
|
|
4252
|
+
* `40P01 deadlock_detected` — iki işlem birbirinin kilidini bekliyordu;
|
|
4253
|
+
* Postgres birini seçip iptal etti.
|
|
4254
|
+
*
|
|
4255
|
+
* Bu hatayı GÖRMEK bir tasarım sinyali: kilit sırası deterministik değilse
|
|
4256
|
+
* tekrar denemek yalnız maliyeti öteler. `Database.$lockRows(...)` (FR-035)
|
|
4257
|
+
* ya da çok satırlı yazmanın CTE'si (FR-015) sırayı sabitler.
|
|
4258
|
+
*/
|
|
4259
|
+
declare class DeadlockDetected extends Conflict implements RetryableError {
|
|
4260
|
+
readonly retryable: true;
|
|
4261
|
+
readonly sqlstate: string;
|
|
4262
|
+
static is(e: unknown): e is DeadlockDetected;
|
|
4263
|
+
constructor(sqlstate?: string, message?: string, data?: unknown);
|
|
4264
|
+
}
|
|
4265
|
+
/** A single field-level validation failure carried by {@link BadRequest}. */
|
|
4266
|
+
interface FieldError {
|
|
4267
|
+
/** The offending field's name (dotted path for nested fields). */
|
|
4268
|
+
field: string;
|
|
4269
|
+
/** Human-readable reason the field failed. */
|
|
4270
|
+
message: string;
|
|
4271
|
+
}
|
|
4272
|
+
/** The fixed, typed payload {@link BadRequest} ships. */
|
|
4273
|
+
interface BadRequestData {
|
|
4274
|
+
/** The fields that failed validation. */
|
|
4275
|
+
fields: FieldError[];
|
|
4276
|
+
}
|
|
4277
|
+
/** The fixed, typed payload {@link TooManyRequests} ships. */
|
|
4278
|
+
interface TooManyRequestsData {
|
|
4279
|
+
/** Seconds the caller should wait before retrying. */
|
|
4280
|
+
retryAfter: number;
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* 429 — the caller has exceeded the rate limit. Carries a fixed typed payload:
|
|
4284
|
+
* `new TooManyRequests({ retryAfter: 30 })`. The shape is declared once in the
|
|
4285
|
+
* SDK (error-registry pre-seed) so codegen surfaces `error.data.retryAfter`
|
|
4286
|
+
* typed on the client — no per-project definition needed.
|
|
4287
|
+
*/
|
|
4288
|
+
declare class TooManyRequests extends NamedHttpError {
|
|
4289
|
+
readonly data: TooManyRequestsData;
|
|
4290
|
+
constructor(data: TooManyRequestsData, message?: string);
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
/** Uploaded file metadata injected into endpoint context when a file is present.
|
|
4294
|
+
* `data` is typed as `Uint8Array` for SDK portability (Buffer extends Uint8Array in Node).
|
|
4295
|
+
*/
|
|
4296
|
+
interface FileContext {
|
|
4297
|
+
filename: string;
|
|
4298
|
+
contentType: string;
|
|
4299
|
+
size: number;
|
|
4300
|
+
data: Uint8Array;
|
|
4301
|
+
}
|
|
4302
|
+
/** Rate limit configuration for an endpoint. */
|
|
4303
|
+
interface RateLimitConfig {
|
|
4304
|
+
/** Maximum number of requests in the window. */
|
|
4305
|
+
max: number;
|
|
4306
|
+
/** Window duration in seconds. */
|
|
4307
|
+
window: number;
|
|
4308
|
+
}
|
|
4309
|
+
/** The six raw string-keyed DB operations shared by `DBClient` and the
|
|
4310
|
+
* transaction-scoped client. */
|
|
4311
|
+
interface DBOps {
|
|
4312
|
+
/**
|
|
4313
|
+
* Run SQL on the REQUEST'S OWN transaction.
|
|
4314
|
+
*
|
|
4315
|
+
* This is **not** read-only. Nothing marks that transaction `READ ONLY`, so a
|
|
4316
|
+
* write passed here executes and commits with the request — a plain `UPDATE`, a
|
|
4317
|
+
* write hidden in a CTE, or a function that writes. What stands between a
|
|
4318
|
+
* statement and your data is RLS and the role the request runs as, exactly as
|
|
4319
|
+
* for every other operation on this client.
|
|
4320
|
+
*
|
|
4321
|
+
* `INSERT … ON CONFLICT DO UPDATE` therefore works here; `tables.<t>.put()`
|
|
4322
|
+
* is the same thing with types. Array parameters are encoded as Postgres array
|
|
4323
|
+
* literals, so `= ANY($1::uuid[])` takes a plain JS array.
|
|
4324
|
+
*
|
|
4325
|
+
* This comment used to claim a `READ ONLY` transaction. It was wrong, and the
|
|
4326
|
+
* cost was measured: a tenant read it, concluded a put was impossible from
|
|
4327
|
+
* here, abandoned `ON CONFLICT` and wrote a database trigger instead.
|
|
4328
|
+
*/
|
|
4329
|
+
/**
|
|
4330
|
+
* Ham SQL. Generic VERİLMEZSE `unknown[]` döner (FR-021).
|
|
4331
|
+
*
|
|
4332
|
+
* Eskiden `Record<string, unknown>[]` dönüyordu ve bu bir YALANDI: ham SQL'in
|
|
4333
|
+
* sonucunu tipleyen tek şey geliştiricinin iddiasıdır, ve hiçbir iddia
|
|
4334
|
+
* yapılmamışsa doğru cevap "bilmiyorum"dur. Drizzle aynı durumda `unknown`
|
|
4335
|
+
* döndürüp yalanı görünür kılıyor; bir satırın alanına erişmek istiyorsanız
|
|
4336
|
+
* tipi açıkça yazın — o zaman iddia sizin, ve okunabilir.
|
|
4337
|
+
*
|
|
4338
|
+
* BU OP READ-ONLY DEĞİLDİR. Adı "query" olduğu için okuma yaptığı varsayılır;
|
|
4339
|
+
* motor bu transaction'ı READ ONLY işaretlemez (`engine/db.ts`), yani buradan
|
|
4340
|
+
* yazma da yapılabilir. Nothing marks that transaction READ ONLY — okuyan
|
|
4341
|
+
* tersini varsaymasın diye açıkça yazılıdır.
|
|
4342
|
+
*/
|
|
4343
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
4344
|
+
insert(table: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
4345
|
+
/**
|
|
4346
|
+
* Insert the row, or update it when it collides on `onConflict` — one
|
|
4347
|
+
* statement, so two concurrent callers cannot both lose.
|
|
4348
|
+
*
|
|
4349
|
+
* Read-then-write cannot be made safe here by catching the unique violation: a
|
|
4350
|
+
* request runs in one transaction, so the failed insert aborts it and every
|
|
4351
|
+
* later statement answers `current transaction is aborted`.
|
|
4352
|
+
*/
|
|
4353
|
+
put(table: string, data: Record<string, unknown>, opts: {
|
|
4354
|
+
onConflict: readonly string[];
|
|
4355
|
+
}): Promise<Record<string, unknown>>;
|
|
4356
|
+
/**
|
|
4357
|
+
* Update the row with the given id and resolve to the updated row, or `null`
|
|
4358
|
+
* if no row matched (the id is absent, or the row is hidden by RLS). This is
|
|
4359
|
+
* an idempotent outcome, not an error — it mirrors `findById`. Map `null` to
|
|
4360
|
+
* a 404 in your service if a missing row should be a client error.
|
|
4361
|
+
*/
|
|
4362
|
+
update(table: string, id: string, data: Record<string, unknown>): Promise<Record<string, unknown> | null>;
|
|
4363
|
+
delete(table: string, id: string): Promise<void>;
|
|
4364
|
+
findById(table: string, id: string): Promise<Record<string, unknown> | null>;
|
|
4365
|
+
/** Hibrit arama (FR-013/014) — tek SQL, RRF; engine implement eder (T017). */
|
|
4366
|
+
search(table: string, params?: {
|
|
4367
|
+
query?: string;
|
|
4368
|
+
vector?: number[];
|
|
4369
|
+
where?: Record<string, unknown>;
|
|
4370
|
+
limit?: number;
|
|
4371
|
+
using?: string;
|
|
4372
|
+
mode?: "hybrid" | "text" | "vector";
|
|
4373
|
+
/** Nihai (RRF-sonrası) skor alt eşiği (FR-001). */
|
|
4374
|
+
minScore?: number;
|
|
4375
|
+
/** RRF-sonrası üstel tazelik çürümesi (FR-004; feed/haber düğmesi). */
|
|
4376
|
+
recency?: {
|
|
4377
|
+
field: string;
|
|
4378
|
+
halfLife: string;
|
|
4379
|
+
};
|
|
4380
|
+
/** Chunk-modunda satır başına en iyi blok sayısı (FR-015). */
|
|
4381
|
+
blocksPerRow?: number;
|
|
4382
|
+
/** @deprecated FR-058: search'ün parametresi DEĞİL — facets() kullanın. */
|
|
4383
|
+
facets?: string[];
|
|
4384
|
+
/** Satır-modunda ts_headline vurgusu: satırlara `_highlight` (FR-025; chunk-modda no-op). */
|
|
4385
|
+
highlight?: boolean;
|
|
4386
|
+
/** Validity'li tabloda zaman penceresi: vars. yalnız güncel; "all" tümü; {asOf} o an (FR-029). */
|
|
4387
|
+
validity?: "all" | {
|
|
4388
|
+
asOf: string;
|
|
4389
|
+
};
|
|
4390
|
+
/** Alan-boost (FR-030): skor * (1 + w·x/(1+x)) — sınırlı çarpan, dış servissiz. */
|
|
4391
|
+
boost?: {
|
|
4392
|
+
field: string;
|
|
4393
|
+
weight: number;
|
|
4394
|
+
};
|
|
4395
|
+
}): Promise<Record<string, unknown>[]>;
|
|
4396
|
+
/** Validity'li tabloda satırın yeni versiyonu — eski kapanır (valid_to/superseded_by),
|
|
4397
|
+
* yenisi eklenir; TEK savepoint, dönüş yeni satır (FR-029, C-9). */
|
|
4398
|
+
supersede(table: string, id: string, row: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
4399
|
+
/**
|
|
4400
|
+
* Update every row the filter matches, in one statement; resolves to them.
|
|
4401
|
+
* An EMPTY filter is refused — a whole-table write is not something to reach
|
|
4402
|
+
* by accident.
|
|
4403
|
+
*
|
|
4404
|
+
* **0 satır dönmesi bir hata değildir, ve BAŞARI da değildir.** Koşullu bir
|
|
4405
|
+
* update'in tamamı burada saklı:
|
|
4406
|
+
*
|
|
4407
|
+
* ```ts
|
|
4408
|
+
* const [row] = await db.updateMany("accounts",
|
|
4409
|
+
* { id, balance: { gte: amount } }, // koşul WHERE'de
|
|
4410
|
+
* { balance: decrement(amount) }); // tek statement
|
|
4411
|
+
* if (row === undefined) throw new Conflict("yetersiz bakiye");
|
|
4412
|
+
* ```
|
|
4413
|
+
*
|
|
4414
|
+
* 1 satır = uygulandı. **0 satır = koşul tutmadı** (satır yok ya da bakiye
|
|
4415
|
+
* yetmedi) — ikisini ayıran tek şey dönen dizidir. Çağrı hata fırlatmadı diye
|
|
4416
|
+
* işlem oldu SAYILAMAZ; kontrol çağıranın işi.
|
|
4417
|
+
*/
|
|
4418
|
+
updateMany(table: string, where: Record<string, unknown>, set: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
4419
|
+
/**
|
|
4420
|
+
* Bir idempotency anahtarını sahiplen (FR-033).
|
|
4421
|
+
*
|
|
4422
|
+
* `{ inserted: true, row }` bu çağrı yazdı; `{ inserted: false, row }` başkası
|
|
4423
|
+
* önce yazdı ve dönen satır ONUN satırı. İkisi de BAŞARILI sonuçtur —
|
|
4424
|
+
* "zaten vardı" bir hata değil, idempotency'nin tanımıdır.
|
|
4425
|
+
*
|
|
4426
|
+
* `unique` satırı BULAN alanlar, `extra` yalnız yazılanlar. Ayrım şart:
|
|
4427
|
+
* ikinci çağrı farklı bir yük gönderirse satır yine anahtarla bulunur.
|
|
4428
|
+
*
|
|
4429
|
+
* Altında `ON CONFLICT` YOKTUR — ölçüldü (pg16): `DO UPDATE` ikincinin
|
|
4430
|
+
* verisiyle birincininkini EZER (10.00 → 999.00), `DO NOTHING` hiçbir satır
|
|
4431
|
+
* DÖNDÜRMEZ. Plain INSERT + 23505 yakalama, birincinin satırını koruyan
|
|
4432
|
+
* tek dal.
|
|
4433
|
+
*/
|
|
4434
|
+
/**
|
|
4435
|
+
* Bu satırları kilitle — sırayı KATMAN koyar (FR-035).
|
|
4436
|
+
*
|
|
4437
|
+
* Uygulama dokunacağı kümeyi önden bildirir; hangi sırayla kilitleneceğine
|
|
4438
|
+
* katman karar verir. Çağıranın sırasına güvenmek, iki çağıranın aynı iki
|
|
4439
|
+
* satıra ters sırayla girmesi ve deadlock demektir — `deadlock_timeout`
|
|
4440
|
+
* varsayılanı 1 saniye, yani kaybeden bir saniye bekleyip hata alır.
|
|
4441
|
+
*
|
|
4442
|
+
* `FOR NO KEY UPDATE` — `updateMany`'nin CTE'siyle aynı mod.
|
|
4443
|
+
*/
|
|
4444
|
+
lockRows(table: string, ids: readonly string[]): Promise<void>;
|
|
4445
|
+
/**
|
|
4446
|
+
* Bir ADI kilitle — satırı değil (FR-054). Kilitlenecek satırı olmayan
|
|
4447
|
+
* işler için ("aynı anda tek bir fatura kapanışı koşsun").
|
|
4448
|
+
*
|
|
4449
|
+
* YALNIZ transaction kapsamlı: commit/rollback'te Postgres bırakır. Oturum
|
|
4450
|
+
* kapsamlı sürüm bağlantı havuza dönerken kilit üstünde kalacağı için
|
|
4451
|
+
* SUNULMUYOR — bir sonraki isteği hiç bitmeyecek şekilde bekletirdi.
|
|
4452
|
+
*/
|
|
4453
|
+
advisoryXactLock(key: string): Promise<void>;
|
|
4454
|
+
claim(table: string, unique: Record<string, unknown>, extra?: Record<string, unknown>): Promise<{
|
|
4455
|
+
inserted: boolean;
|
|
4456
|
+
row: Record<string, unknown>;
|
|
4457
|
+
}>;
|
|
4458
|
+
/** Delete every row the filter matches; resolves to how many. Empty filter
|
|
4459
|
+
* refused, for the same reason and more so. */
|
|
4460
|
+
deleteMany(table: string, where: Record<string, unknown>): Promise<number>;
|
|
4461
|
+
/** How many rows match — the half of pagination limit/offset cannot supply.
|
|
4462
|
+
* An empty filter is legitimate here: counting is a read. */
|
|
4463
|
+
count(table: string, where?: Record<string, unknown>): Promise<number>;
|
|
4464
|
+
/** Filtre + sıralama + sayfa. `offset` yalnız `limit` ile birlikte geçerlidir
|
|
4465
|
+
* (limitsiz offset sayfa değildir; motor adıyla reddeder — FR-004). */
|
|
4466
|
+
findMany(table: string, query?: Record<string, unknown>, opts?: {
|
|
4467
|
+
orderBy?: {
|
|
4468
|
+
column: string;
|
|
4469
|
+
direction?: "asc" | "desc";
|
|
4470
|
+
};
|
|
4471
|
+
limit?: number;
|
|
4472
|
+
offset?: number;
|
|
4473
|
+
}): Promise<Record<string, unknown>[]>;
|
|
4474
|
+
/** "Bu satıra benzer satırlar" (FR-022) — satır-modunda kolon vektörüyle,
|
|
4475
|
+
* chunk-modunda chunk-ortalamasıyla; kendisi hariç. */
|
|
4476
|
+
similar(table: string, id: string, opts?: {
|
|
4477
|
+
limit?: number;
|
|
4478
|
+
where?: Record<string, unknown>;
|
|
4479
|
+
minScore?: number;
|
|
4480
|
+
blocksPerRow?: number;
|
|
4481
|
+
validity?: "all" | {
|
|
4482
|
+
asOf: string;
|
|
4483
|
+
};
|
|
4484
|
+
boost?: {
|
|
4485
|
+
field: string;
|
|
4486
|
+
weight: number;
|
|
4487
|
+
};
|
|
4488
|
+
}): Promise<Record<string, unknown>[]>;
|
|
4489
|
+
/** D-021 (FR-027): kolon sayaçları bağımsız dönüşle — search'ün dizi-üstü
|
|
4490
|
+
* `_facets` özelliği serileşmede kaybolur; ciddi sözleşme budur. */
|
|
4491
|
+
facets(table: string, params: {
|
|
4492
|
+
facets: string[];
|
|
4493
|
+
where?: Record<string, unknown>;
|
|
4494
|
+
validity?: "all" | {
|
|
4495
|
+
asOf: string;
|
|
4496
|
+
};
|
|
4497
|
+
}): Promise<Record<string, {
|
|
4498
|
+
value: string | null;
|
|
4499
|
+
count: number;
|
|
4500
|
+
}[]>>;
|
|
4501
|
+
/** Çok-örnekli öneri (FR-023): avg(pos) + (avg(pos) − avg(neg)); positive'ler sonuç dışı. */
|
|
4502
|
+
recommend(table: string, opts: {
|
|
4503
|
+
positive: string[];
|
|
4504
|
+
negative?: string[];
|
|
4505
|
+
limit?: number;
|
|
4506
|
+
where?: Record<string, unknown>;
|
|
4507
|
+
minScore?: number;
|
|
4508
|
+
blocksPerRow?: number;
|
|
4509
|
+
validity?: "all" | {
|
|
4510
|
+
asOf: string;
|
|
4511
|
+
};
|
|
4512
|
+
boost?: {
|
|
4513
|
+
field: string;
|
|
4514
|
+
weight: number;
|
|
4515
|
+
};
|
|
4516
|
+
}): Promise<Record<string, unknown>[]>;
|
|
4517
|
+
}
|
|
4518
|
+
/** Database client interface injected into endpoint context. */
|
|
4519
|
+
interface DBClient extends DBOps {
|
|
4520
|
+
/**
|
|
4521
|
+
* Run a whole transaction as ONE plan, executed on the request's own
|
|
4522
|
+
* transaction inside a savepoint.
|
|
4523
|
+
*
|
|
4524
|
+
* The low-level seam behind `Database.$transaction(plan)`: the SDK builds the
|
|
4525
|
+
* plan, this sends it, and the broker executes every operation inside a single
|
|
4526
|
+
* transaction that commits or rolls back before the response is written.
|
|
4527
|
+
* Nothing pins a Postgres backend across round trips, because there is only
|
|
4528
|
+
* one round trip.
|
|
4529
|
+
*
|
|
4530
|
+
* On failure the runtime must reject with an error carrying the broker's
|
|
4531
|
+
* envelope fields — see {@link TxPlanRejection}. `slot` in particular is what
|
|
4532
|
+
* turns a `tx_guard_failed` back into the `Error` the tenant handed to
|
|
4533
|
+
* `.expectOne(…)`; without it a declared expectation degrades to a generic 409.
|
|
4534
|
+
*/
|
|
4535
|
+
txPlan(plan: TxPlanBody): Promise<TxPlanResponse>;
|
|
4536
|
+
/**
|
|
4537
|
+
* Run `fn` inside a SAVEPOINT on this request's transaction, so a failure in
|
|
4538
|
+
* it rolls back only what `fn`'s handle wrote.
|
|
4539
|
+
*
|
|
4540
|
+
* The handle is a PARAMETER on purpose: an ambient swap would capture writes
|
|
4541
|
+
* a concurrent `Promise.all` branch made outside the boundary.
|
|
4542
|
+
*/
|
|
4543
|
+
attempt<T>(fn: (tx: DBOps) => Promise<T>): Promise<T>;
|
|
4544
|
+
/**
|
|
4545
|
+
* Return a sibling DB client that bypasses Row-Level Security by running as
|
|
4546
|
+
* the `service_role` (BYPASSRLS). Use sparingly and explicitly — the default
|
|
4547
|
+
* `Database.*` path is RLS-enforced. The returned client exposes the same op
|
|
4548
|
+
* surface (`query`/`insert`/.../`txPlan`) but never re-exposes `asService`
|
|
4549
|
+
* (no double-bypass).
|
|
4550
|
+
*/
|
|
4551
|
+
asService(): Omit<DBClient, "asService">;
|
|
4552
|
+
}
|
|
4553
|
+
/** Logger interface injected into endpoint context. */
|
|
4554
|
+
interface Logger {
|
|
4555
|
+
info(message: string, ...args: unknown[]): void;
|
|
4556
|
+
warn(message: string, ...args: unknown[]): void;
|
|
4557
|
+
error(message: string, ...args: unknown[]): void;
|
|
4558
|
+
debug(message: string, ...args: unknown[]): void;
|
|
4559
|
+
}
|
|
4560
|
+
/**
|
|
4561
|
+
* Cache client interface injected into endpoint context.
|
|
4562
|
+
*
|
|
4563
|
+
* The cache is JSON-typed: values are serialized to/from JSON, so any JSON
|
|
4564
|
+
* value (objects, arrays, numbers, booleans, strings) round-trips. `get<T>`
|
|
4565
|
+
* therefore returns `T | null` rather than `string | null`.
|
|
4566
|
+
*/
|
|
4567
|
+
/**
|
|
4568
|
+
* The tenant's secrets, as a controller sees them.
|
|
4569
|
+
*
|
|
4570
|
+
* `get` answers null for a name this tenant has not set — including every name
|
|
4571
|
+
* the STACK holds, which is not an accident of storage but the shape of the
|
|
4572
|
+
* surface: the runtime is handed a tenant client, and there is no route that
|
|
4573
|
+
* returns a platform secret's value at all. Tenant code cannot read the keys
|
|
4574
|
+
* the stack signs with, by mistake or on purpose.
|
|
4575
|
+
*
|
|
4576
|
+
* Reads are cheap. The value is held in the process and dropped when the deploy
|
|
4577
|
+
* pointer reports a new secrets generation, so a rotated secret arrives without
|
|
4578
|
+
* a restart and a hot path pays a map lookup.
|
|
4579
|
+
*/
|
|
4580
|
+
interface SecretsService {
|
|
4581
|
+
/**
|
|
4582
|
+
* A secret's value, or `null` when this backend's vault holds no value under
|
|
4583
|
+
* that name.
|
|
4584
|
+
*
|
|
4585
|
+
* The name is a `PalbaseSecretName`, not a `string`: it comes from the
|
|
4586
|
+
* generated `palbase-stack.d.ts`, so a secret the stack does not hold cannot
|
|
4587
|
+
* be spelled here at all. THE GATE IS THE COMPILER.
|
|
4588
|
+
*
|
|
4589
|
+
* That gate replaced a declaration. `config/secrets.ts` used to restate, in
|
|
4590
|
+
* the repo, names the vault already held, and the push compared the two lists
|
|
4591
|
+
* — a check that only ran at deploy, and only if the author remembered to
|
|
4592
|
+
* declare. The type runs on every keystroke and cannot be forgotten.
|
|
4593
|
+
*/
|
|
4594
|
+
get(name: PalbaseSecretName): Promise<string | null>;
|
|
4595
|
+
}
|
|
4596
|
+
interface CacheClient {
|
|
4597
|
+
/** Read a value. Returns `null` on a cache miss. */
|
|
4598
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
4599
|
+
/** Write a JSON-serializable value with an optional TTL (seconds). */
|
|
4600
|
+
set(key: string, value: unknown, ttl?: number): Promise<void>;
|
|
4601
|
+
/** Delete a key. */
|
|
4602
|
+
del(key: string): Promise<void>;
|
|
4603
|
+
/** Atomically increment an integer counter, returning the new value. */
|
|
4604
|
+
incr(key: string): Promise<number>;
|
|
4605
|
+
/**
|
|
4606
|
+
* Stampede-safe read-through cache fill. On a hit, returns the cached value.
|
|
4607
|
+
* On a miss, a single caller (across all pod replicas, coordinated by a
|
|
4608
|
+
* distributed lock) runs `fn`, caches the result for `ttl` seconds, and
|
|
4609
|
+
* returns it; concurrent callers wait for that result instead of also
|
|
4610
|
+
* running `fn`. If no value lands within the lock's TTL, the call rejects —
|
|
4611
|
+
* it does NOT run `fn` on timeout (that would reintroduce the stampede).
|
|
4612
|
+
*
|
|
4613
|
+
* @param ttl value TTL in seconds.
|
|
4614
|
+
*/
|
|
4615
|
+
getOrSet<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T>;
|
|
4616
|
+
}
|
|
4617
|
+
/** Result envelope used across the Palbase Server SDK clients. */
|
|
4618
|
+
interface PalbaseResult<T> {
|
|
4619
|
+
data: T | null;
|
|
4620
|
+
error: {
|
|
4621
|
+
message?: string;
|
|
4622
|
+
code?: string;
|
|
4623
|
+
} | null;
|
|
4624
|
+
status?: number;
|
|
4625
|
+
}
|
|
4626
|
+
/** Document snapshot returned by docs.get(). */
|
|
4627
|
+
interface PalbaseDocumentSnapshot<T = Record<string, unknown>> {
|
|
4628
|
+
id: string;
|
|
4629
|
+
exists: boolean;
|
|
4630
|
+
data(): T | undefined;
|
|
4631
|
+
ref: {
|
|
4632
|
+
path: string;
|
|
4633
|
+
};
|
|
4634
|
+
}
|
|
4635
|
+
/** Collection query snapshot returned by collection.get(). */
|
|
4636
|
+
interface PalbaseQuerySnapshot<T = Record<string, unknown>> {
|
|
4637
|
+
docs: PalbaseDocumentSnapshot<T>[];
|
|
4638
|
+
empty: boolean;
|
|
4639
|
+
size: number;
|
|
4640
|
+
/** Firestore-shaped change list. Every document of a one-shot read is "added". */
|
|
4641
|
+
docChanges(): Array<{
|
|
4642
|
+
type: "added";
|
|
4643
|
+
doc: PalbaseDocumentSnapshot<T>;
|
|
4644
|
+
}>;
|
|
4645
|
+
}
|
|
4646
|
+
/** Comparison operators supported by docs.where(). */
|
|
4647
|
+
type PalbaseWhereOperator = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "array-contains";
|
|
4648
|
+
/** Document reference exposed by Documents.collection(...).doc(...). */
|
|
4649
|
+
interface PalbaseDocumentRef<T = Record<string, unknown>> {
|
|
4650
|
+
readonly path: string;
|
|
4651
|
+
set(data: T): Promise<PalbaseResult<void>>;
|
|
4652
|
+
get(): Promise<PalbaseResult<PalbaseDocumentSnapshot<T>>>;
|
|
4653
|
+
update(data: Partial<T>): Promise<PalbaseResult<void>>;
|
|
4654
|
+
delete(): Promise<PalbaseResult<void>>;
|
|
4655
|
+
/** A subcollection of this document — `rooms/1` → `rooms/1/messages`. */
|
|
4656
|
+
collection<C extends Record<string, unknown> = Record<string, unknown>>(name: string): PalbaseCollectionRef<C>;
|
|
4657
|
+
}
|
|
4658
|
+
/** Collection reference exposed by Documents.collection(...). */
|
|
4659
|
+
interface PalbaseCollectionRef<T = Record<string, unknown>> {
|
|
4660
|
+
readonly path: string;
|
|
4661
|
+
doc(id: string): PalbaseDocumentRef<T>;
|
|
4662
|
+
add(data: T): Promise<PalbaseResult<PalbaseDocumentRef<T>>>;
|
|
4663
|
+
where(field: string, op: PalbaseWhereOperator, value: unknown): PalbaseCollectionRef<T>;
|
|
4664
|
+
orderBy(field: string, direction?: "asc" | "desc"): PalbaseCollectionRef<T>;
|
|
4665
|
+
limit(n: number): PalbaseCollectionRef<T>;
|
|
4666
|
+
get(): Promise<PalbaseResult<PalbaseQuerySnapshot<T>>>;
|
|
4667
|
+
}
|
|
4668
|
+
/** Docs client surface available on the Documents singleton. */
|
|
4669
|
+
interface PalbaseDocsClient {
|
|
4670
|
+
collection<T extends Record<string, unknown> = Record<string, unknown>>(path: string): PalbaseCollectionRef<T>;
|
|
4671
|
+
doc<T extends Record<string, unknown> = Record<string, unknown>>(path: string): PalbaseDocumentRef<T>;
|
|
4672
|
+
/**
|
|
4673
|
+
* Apply up to 500 writes in one call.
|
|
4674
|
+
*
|
|
4675
|
+
* Declared here because the client has always offered it: an interface that
|
|
4676
|
+
* described less than the implementation is how `Documents.doc(...)` came to
|
|
4677
|
+
* be declared and never implemented — the gap this whole move exists to close.
|
|
4678
|
+
*/
|
|
4679
|
+
batch(operations: Array<{
|
|
4680
|
+
op: "set" | "update" | "delete";
|
|
4681
|
+
ref: {
|
|
4682
|
+
path: string;
|
|
4683
|
+
};
|
|
4684
|
+
data?: unknown;
|
|
4685
|
+
}>): Promise<PalbaseResult<void>>;
|
|
4686
|
+
}
|
|
4687
|
+
/** Calling-client metadata, derived from request headers.
|
|
4688
|
+
*
|
|
4689
|
+
* Every header is named here, because the previous version of this comment named
|
|
4690
|
+
* only two of the four and the next reader invented `X-Palbase-Platform` for the
|
|
4691
|
+
* third. The four are canonical across the platform — the same names the Go
|
|
4692
|
+
* side reads in user-flags/internal/middleware/clientcontext.go — and the deploy
|
|
4693
|
+
* gate REFUSES an `@Headers` schema that declares an `x-palbase-*` key, which
|
|
4694
|
+
* makes `@Client()` the only sanctioned way to read them.
|
|
4695
|
+
*
|
|
4696
|
+
* All fields are nullable: a request may come from a non-SDK caller (curl,
|
|
4697
|
+
* server-to-server) that sends none of them. Note that the web SDK does not
|
|
4698
|
+
* send `X-Palbase-Sdk-Version` at all, so `sdkVersion` is null for every browser
|
|
4699
|
+
* caller. The semver comparison helpers (`appVersionAtLeast`, …) arrive in
|
|
4700
|
+
* Phase 2 — Phase 1 surfaces only the raw data fields. */
|
|
4701
|
+
interface ClientInfo {
|
|
4702
|
+
/** Palbase SDK version (`X-Palbase-Sdk-Version`), or null. */
|
|
4703
|
+
sdkVersion: string | null;
|
|
4704
|
+
/** Calling app's own version (`X-Palbase-Client-Version`), or null. */
|
|
4705
|
+
appVersion: string | null;
|
|
4706
|
+
/** Platform identifier (`X-Platform`, e.g. "ios", "android", "web"), or null. */
|
|
4707
|
+
platform: string | null;
|
|
4708
|
+
/** OS version string (`X-OS-Version`), or null. */
|
|
4709
|
+
osVersion: string | null;
|
|
4710
|
+
}
|
|
4711
|
+
/** Palbase module clients, as a structural bundle.
|
|
4712
|
+
*
|
|
4713
|
+
* NOT part of the endpoint surface anymore — endpoint handlers reach services
|
|
4714
|
+
* via the PascalCase singletons (`Database`, `Documents`, …). This type is
|
|
4715
|
+
* retained as an INTERNAL shape for the sibling contexts that still carry a
|
|
4716
|
+
* `ctx` (middleware, jobs, workers, hooks, webhooks — out of Phase 1 scope).
|
|
4717
|
+
* It is intentionally not re-exported from `index.ts`.
|
|
4718
|
+
*
|
|
4719
|
+
* Structurally typed against the runtime's `ServerClient`; the runtime injects
|
|
4720
|
+
* the real client, so mismatched names would surface as `undefined is not a
|
|
4721
|
+
* function` at call time — keep these in sync with `ServerClient`. */
|
|
4722
|
+
interface PalbaseModuleClients {
|
|
4723
|
+
auth: PalbaseAuthClient;
|
|
4724
|
+
storage: PalbaseStorageClient;
|
|
4725
|
+
docs: PalbaseDocsClient;
|
|
4726
|
+
realtime: PalbaseRealtimeClient;
|
|
4727
|
+
functions: PalbaseFunctionsClient;
|
|
4728
|
+
flags: PalbaseFlagsClient;
|
|
4729
|
+
notifications: PalbaseNotificationsClient;
|
|
4730
|
+
analytics: PalbaseAnalyticsClient;
|
|
4731
|
+
links: PalbaseLinksClient;
|
|
4732
|
+
}
|
|
4733
|
+
/** Declared-error definition.
|
|
4734
|
+
*
|
|
4735
|
+
* `code` is the stable snake_case identifier that lands on the wire envelope's
|
|
4736
|
+
* `error` field. `status` is the HTTP status code returned. `data`, if set,
|
|
4737
|
+
* is a Zod schema whose value rides on the envelope's `data` field — the CLI
|
|
4738
|
+
* codegen lowers it to the typed enum's associated value on iOS.
|
|
4739
|
+
*
|
|
4740
|
+
* `description` is optional human-readable text; the OpenAPI generator uses it
|
|
4741
|
+
* for the response description and the iOS codegen surfaces it in the
|
|
4742
|
+
* generated method's doc-comment.
|
|
4743
|
+
*/
|
|
4744
|
+
interface ErrorDef<TData extends ZodSchema = ZodSchema> {
|
|
4745
|
+
status: number;
|
|
4746
|
+
code: string;
|
|
4747
|
+
description?: string;
|
|
4748
|
+
data?: TData;
|
|
4749
|
+
}
|
|
4750
|
+
/** Map of declared errors keyed by their TypeScript-side names.
|
|
4751
|
+
*
|
|
4752
|
+
* Keys are the friendly names the handler uses (`req.errors.todoLocked`);
|
|
4753
|
+
* `code` on each value is the wire identifier. The TS name is what the iOS
|
|
4754
|
+
* codegen lowers to (camelCase enum cases), and the wire `code` is what the
|
|
4755
|
+
* envelope's `error` field carries.
|
|
4756
|
+
*/
|
|
4757
|
+
type ErrorMap = Record<string, ErrorDef>;
|
|
4758
|
+
/** Throwable proxy projected onto `req.errors` when `errors` is declared.
|
|
4759
|
+
*
|
|
4760
|
+
* Each entry is a constructor: declared errors with a `data` schema demand
|
|
4761
|
+
* the payload as a required argument; declared errors without `data` take
|
|
4762
|
+
* none. Throwing the result emits the standard envelope (with `data` when
|
|
4763
|
+
* present).
|
|
4764
|
+
*
|
|
4765
|
+
* throw req.errors.todoNotFound();
|
|
4766
|
+
* throw req.errors.todoLocked({ retryAfter: 30 });
|
|
4767
|
+
*/
|
|
4768
|
+
type ErrorThrowers<TErrors extends ErrorMap | undefined> = TErrors extends ErrorMap ? {
|
|
4769
|
+
[K in keyof TErrors]: TErrors[K]["data"] extends ZodSchema ? (data: z.infer<NonNullable<TErrors[K]["data"]>>) => HttpError : () => HttpError;
|
|
4770
|
+
} : Record<string, never>;
|
|
4771
|
+
/** The request-scoped object passed to every endpoint handler.
|
|
4772
|
+
*
|
|
4773
|
+
* Replaces the old `ctx` god-object. `PBRequest` carries ONLY request-scoped
|
|
4774
|
+
* data — the typed `input`, route/query params, headers, the authenticated
|
|
4775
|
+
* `user`, calling-client metadata, trace ids, and the endpoint's declared
|
|
4776
|
+
* error throwers. Services (`Database`, `Documents`, `Cache`, …) are NOT on
|
|
4777
|
+
* the request: import them directly from `@palbase/backend` as singletons.
|
|
4778
|
+
*
|
|
4779
|
+
* import { Controller, Get, Req, Database } from "@palbase/backend";
|
|
4780
|
+
*
|
|
4781
|
+
* \@Controller("/todos")
|
|
4782
|
+
* export class TodosController {
|
|
4783
|
+
* \@Get("") list(\@Req() req: PBRequest): unknown {
|
|
4784
|
+
* return Database.$findMany("todos");
|
|
4785
|
+
* }
|
|
4786
|
+
* }
|
|
4787
|
+
*
|
|
4788
|
+
* Most controller methods reach individual request slices via their own
|
|
4789
|
+
* parameter decorator (`@Body`/`@QueryParams`/`@Param`/`@User`/…); `@Req()` is the
|
|
4790
|
+
* escape hatch that injects this whole object.
|
|
4791
|
+
*
|
|
4792
|
+
* Generic parameters:
|
|
4793
|
+
* - `TInput` — the validated `input` type (the `@Body` schema's `z.infer`). The
|
|
4794
|
+
* user-facing form is single-generic: `PBRequest<TodoInput>`.
|
|
4795
|
+
* - `TAuthed` — whether `user` is non-null. DEFAULTS to `true` (the common
|
|
4796
|
+
* case; the auth pipeline returns 401 before the handler when auth is
|
|
4797
|
+
* required, so a non-null `user` is runtime-honest). A route whose effective
|
|
4798
|
+
* auth is `false` yields `User | null`.
|
|
4799
|
+
* - `TErrors` — RETAINED for back-compat of the `errors` thrower shape; the
|
|
4800
|
+
* class-controller model throws global error classes
|
|
4801
|
+
* (`Conflict`/`NotFound`/…) instead, so `req.errors` is empty in practice.
|
|
4802
|
+
*/
|
|
4803
|
+
interface PBRequest<TInput = unknown, TAuthed extends boolean = true, TErrors extends ErrorMap | undefined = undefined> {
|
|
4804
|
+
/** Validated request input (body for POST/PUT/PATCH; `{}` otherwise). */
|
|
4805
|
+
input: TInput;
|
|
4806
|
+
/** Matched route params (e.g. `{ id }` for `/todos/[id]`). */
|
|
4807
|
+
params: Record<string, string>;
|
|
4808
|
+
/** Parsed query-string params. */
|
|
4809
|
+
query: Record<string, string>;
|
|
4810
|
+
/** Request headers (lowercase keys). */
|
|
4811
|
+
headers: Record<string, string>;
|
|
4812
|
+
/** Authenticated user. Non-null (`User`) by default; `User | null` only when
|
|
4813
|
+
* the route's effective auth disables enforcement (driven by `TAuthed`, which
|
|
4814
|
+
* the runtime resolves from the route/controller `auth` cascade via
|
|
4815
|
+
* {@link IsAuthed}). */
|
|
4816
|
+
user: TAuthed extends true ? User : User | null;
|
|
4817
|
+
/** Calling-client metadata derived from request headers (all nullable). */
|
|
4818
|
+
client: ClientInfo;
|
|
4819
|
+
/** Uploaded file, or null when the request has no file part. */
|
|
4820
|
+
file: FileContext | null;
|
|
4821
|
+
/** HTTP method of the incoming request (e.g. "GET", "POST"). */
|
|
4822
|
+
method: string;
|
|
4823
|
+
/** Per-request id (`req_<…>`), preserved for back-compat correlation. */
|
|
4824
|
+
requestId: string;
|
|
4825
|
+
/** W3C trace id (primary correlation key across modules). */
|
|
4826
|
+
traceId: string;
|
|
4827
|
+
/** W3C span id for this handler invocation. */
|
|
4828
|
+
spanId: string;
|
|
4829
|
+
/** Typed throwers for the endpoint's declared errors. RETAINED for the
|
|
4830
|
+
* `@Req()` escape-hatch shape; the class-controller model throws global error
|
|
4831
|
+
* classes (`Conflict`/`NotFound`/…) instead, so this is empty in practice. */
|
|
4832
|
+
errors: ErrorThrowers<TErrors>;
|
|
4833
|
+
}
|
|
4834
|
+
/** Middleware function signature — uses MiddlewareContext (no input, not yet validated). */
|
|
4835
|
+
type Middleware = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise<void>;
|
|
4836
|
+
/** The shape an endpoint's `auth` config may take.
|
|
4837
|
+
*
|
|
4838
|
+
* Either a bare boolean (`true`/`false`) or an object form (`{ required?,
|
|
4839
|
+
* role? }`). The object form is `Partial<AuthConfig>` so `required` may be
|
|
4840
|
+
* omitted — which the runtime treats as `required: true` (see {@link IsAuthed}).
|
|
4841
|
+
*/
|
|
4842
|
+
type AuthSpec = boolean | Partial<AuthConfig>;
|
|
4843
|
+
|
|
4844
|
+
export { Notifications as $, type AuthSpec as A, BadRequest as B, type CacheClient as C, type DBClient as D, EXTENSION_DEPENDENCIES as E, type ErrorThrowers as F, type FileContext as G, HttpError as H, Flags as I, Forbidden as J, type HttpMethod as K, IndexBuilder as L, type IndexDef as M, type InsertShape as N, type LifecycleHook as O, type PalbaseResult as P, Log as Q, type RateLimitConfig as R, type SchemaDef as S, type TxPlanBody as T, type Logger as U, type Materialized as V, type Middleware as W, type MiddlewareContext as X, type MiddlewareHandler as Y, NotFound as Z, __runWithRuntime as _, type DBOps as a, type PalbaseListOptions as a$, type OnDeleteAction as a0, PALBASE_EXTENSIONS as a1, type PBRequest as a2, PalError as a3, type PalbaseAnalyticsClient as a4, type PalbaseAnalyticsManagementNamespace as a5, type PalbaseAnalyticsProperties as a6, type PalbaseAnalyticsQueryNamespace as a7, type PalbaseAttestAndroidParams as a8, type PalbaseAttestAndroidResult as a9, type PalbaseExtension as aA, type PalbaseFileObject as aB, type PalbaseFlag as aC, type PalbaseFlagContext as aD, type PalbaseFlagSource as aE, type PalbaseFlagValue as aF, type PalbaseFlagVariant as aG, type PalbaseFlagsClient as aH, type PalbaseFlagsServiceClient as aI, type PalbaseFunctionsClient as aJ, type PalbaseFunnelQueryInput as aK, type PalbaseFunnelResult as aL, type PalbaseIdentifyTraits as aM, type PalbaseInboxClient as aN, type PalbaseInboxListOptions as aO, type PalbaseInboxListResult as aP, type PalbaseInboxMessage as aQ, type PalbaseInboxSendParams as aR, type PalbaseInboxSendResponse as aS, type PalbaseInitialLink as aT, type PalbaseInvokeOptions as aU, type PalbaseLink as aV, type PalbaseLinkAnalytics as aW, type PalbaseLinkDetails as aX, type PalbaseLinksClient as aY, type PalbaseListLinksOptions as aZ, type PalbaseListLinksResult as a_, type PalbaseAttestiOSParams as aa, type PalbaseAttestiOSResult as ab, type PalbaseAuthClient as ac, type PalbaseBatchOverrideOperation as ad, type PalbaseBatchSetOverridesResult as ae, type PalbaseBindDeviceParams as af, type PalbaseBucketClient as ag, type PalbaseClearAllOverridesResult as ah, type PalbaseClearOverrideResult as ai, type PalbaseCohortQueryInput as aj, type PalbaseCohortResult as ak, type PalbaseCollectionRef as al, type PalbaseCountQueryInput as am, type PalbaseCountResult as an, type PalbaseCreateLinkParams as ao, type PalbaseDeviceInfo as ap, type PalbaseDeviceTokenView as aq, type PalbaseDocsClient as ar, type PalbaseDocumentRef as as, type PalbaseDocumentSnapshot as at, type PalbaseEmailClient as au, type PalbaseEmailSendParams as av, type PalbaseEmailSendResponse as aw, type PalbaseEventNamesResult as ax, type PalbaseEventsQueryInput as ay, type PalbaseEventsResult as az, type TxPlanResponse as b, TxPlanError as b$, type PalbaseMatchParams as b0, type PalbaseMultiChannelResponse as b1, type PalbaseNotificationsClient as b2, type PalbaseOverviewResult as b3, type PalbasePreferences as b4, type PalbasePreferencesClient as b5, type PalbasePushClient as b6, type PalbasePushSendParams as b7, type PalbasePushSendResponse as b8, type PalbaseQrCodeOptions as b9, type PolicyExprCtx as bA, PolicyExprRef as bB, type PolicyMode as bC, type PolicyOperand as bD, type RawConstraintDef as bE, Realtime as bF, type Ref as bG, type RequestStore as bH, type RowShape as bI, Secrets as bJ, type SecretsService as bK, SerializationFailure as bL, type SetShape as bM, type SetValue as bN, type ShutdownRunner as bO, type SqlFragment as bP, Storage as bQ, TABLE_META as bR, type TableDef as bS, type TableHandle as bT, type TableInput as bU, TooManyRequests as bV, type TxColumnExpr as bW, type TxInsertShape as bX, type TxInsertValue as bY, type TxNow as bZ, type TxPlan as b_, type PalbaseQuerySnapshot as ba, type PalbaseRealtimeClient as bb, type PalbaseRegisterDeviceParams as bc, type PalbaseRetentionQueryInput as bd, type PalbaseRetentionResult as be, type PalbaseSession as bf, type PalbaseSetOverrideResult as bg, type PalbaseSetOverridesResult as bh, type PalbaseSignedUrlResponse as bi, type PalbaseSmsClient as bj, type PalbaseSmsSendParams as bk, type PalbaseSmsSendResponse as bl, type PalbaseStorageClient as bm, type PalbaseTransformOptions as bn, type PalbaseUpdateLinkParams as bo, type PalbaseUploadOptions as bp, type PalbaseUser as bq, type PalbaseUserDetailResult as br, type PalbaseUsersQueryInput as bs, type PalbaseUsersResult as bt, type PalbaseVerifyRequestSignatureParams as bu, type PalbaseWhereOperator as bv, type PolicyBinOp as bw, PolicyBuilder as bx, type PolicyCommand as by, type PolicyDef as bz, type RuntimeServices as c, type TxPlanHandle as c0, type TxPlanOpResult as c1, type TxPlanRejection as c2, TxRefError as c3, type TxRow as c4, type TxRows as c5, type TxSelectOptions as c6, type TxSetShape as c7, type TxSetValue as c8, type TxTable as c9, enumType as cA, exprCtx as cB, inc as cC, increment as cD, index as cE, installationRef as cF, integer as cG, isPalbaseExtension as cH, isRetryable as cI, jsonb as cJ, makeTypedDB as cK, now as cL, numeric as cM, onShutdown as cN, onStart as cO, openai as cP, ownedByUser as cQ, policy as cR, raw as cS, sqlFragment as cT, text as cU, timestamp as cV, userRef as cW, uuid as cX, vector as cY, withRetry as cZ, type TxTables as ca, type TxWhere as cb, type TxWireExpr as cc, type TxWireGuard as cd, type TxWireOp as ce, type TxWireRef as cf, type TxWireValue as cg, type TypedDB as ch, type TypedTable as ci, type TypedTx as cj, Unauthorized as ck, UniqueViolation as cl, type User as cm, type VerifiedDevice as cn, __getRuntime as co, __resetLifecycleHooks as cp, __runStartHooks as cq, __setRuntime as cr, bigint as cs, boolean as ct, col as cu, dec as cv, decrement as cw, defineMiddleware as cx, defineSchema as cy, defineTable as cz, __requestALS as d, type PolicyExpr as e, type AnyColumn as f, type AuthConfig as g, Cache as h, type ClientInfo as i, type ColRef as j, ColumnBuilder as k, type ColumnDef as l, type ColumnMap as m, type ColumnType as n, Conflict as o, Database as p, DeadlockDetected as q, Documents as r, type EmbeddingModelRef as s, type EnvSchemas as t, type EnvServiceDatabase as u, type EnvTables as v, type EnvTypedDatabase as w, type EnvTypedTable as x, type ErrorDef as y, type ErrorMap as z };
|