@pawabase/client 0.1.3 → 0.1.4

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.
Files changed (138) hide show
  1. package/README.md +7 -10
  2. package/dist/cjs/admin.d.ts +70 -0
  3. package/dist/cjs/admin.d.ts.map +1 -1
  4. package/dist/cjs/admin.js +75 -1
  5. package/dist/cjs/admin.js.map +1 -1
  6. package/dist/cjs/ai.d.ts +73 -0
  7. package/dist/cjs/ai.d.ts.map +1 -0
  8. package/dist/cjs/ai.js +120 -0
  9. package/dist/cjs/ai.js.map +1 -0
  10. package/dist/cjs/auth.d.ts +38 -0
  11. package/dist/cjs/auth.d.ts.map +1 -1
  12. package/dist/cjs/auth.js +52 -0
  13. package/dist/cjs/auth.js.map +1 -1
  14. package/dist/cjs/client.d.ts +18 -3
  15. package/dist/cjs/client.d.ts.map +1 -1
  16. package/dist/cjs/client.js +30 -3
  17. package/dist/cjs/client.js.map +1 -1
  18. package/dist/cjs/data.d.ts +28 -0
  19. package/dist/cjs/data.d.ts.map +1 -1
  20. package/dist/cjs/data.js +42 -7
  21. package/dist/cjs/data.js.map +1 -1
  22. package/dist/cjs/errors.d.ts +1 -1
  23. package/dist/cjs/errors.d.ts.map +1 -1
  24. package/dist/cjs/errors.js +8 -1
  25. package/dist/cjs/errors.js.map +1 -1
  26. package/dist/cjs/functions.d.ts +0 -28
  27. package/dist/cjs/functions.d.ts.map +1 -1
  28. package/dist/cjs/functions.js +1 -42
  29. package/dist/cjs/functions.js.map +1 -1
  30. package/dist/cjs/index.d.ts +4 -2
  31. package/dist/cjs/index.d.ts.map +1 -1
  32. package/dist/cjs/index.js +3 -2
  33. package/dist/cjs/index.js.map +1 -1
  34. package/dist/cjs/manage/access.d.ts +3 -3
  35. package/dist/cjs/manage/access.d.ts.map +1 -1
  36. package/dist/cjs/manage/access.js +3 -3
  37. package/dist/cjs/manage/access.js.map +1 -1
  38. package/dist/cjs/manage/ai.d.ts +90 -0
  39. package/dist/cjs/manage/ai.d.ts.map +1 -0
  40. package/dist/cjs/manage/ai.js +101 -0
  41. package/dist/cjs/manage/ai.js.map +1 -0
  42. package/dist/cjs/manage/data.d.ts +1 -1
  43. package/dist/cjs/manage/data.d.ts.map +1 -1
  44. package/dist/cjs/manage/data.js +1 -1
  45. package/dist/cjs/manage/data.js.map +1 -1
  46. package/dist/cjs/manage/index.d.ts +2 -3
  47. package/dist/cjs/manage/index.d.ts.map +1 -1
  48. package/dist/cjs/manage/index.js +4 -4
  49. package/dist/cjs/manage/index.js.map +1 -1
  50. package/dist/cjs/manage/mail.d.ts +1 -1
  51. package/dist/cjs/manage/mail.d.ts.map +1 -1
  52. package/dist/cjs/manage/mail.js +1 -1
  53. package/dist/cjs/manage/mail.js.map +1 -1
  54. package/dist/cjs/types.d.ts +3 -3
  55. package/dist/cjs/types.d.ts.map +1 -1
  56. package/dist/cjs/version.d.ts +1 -1
  57. package/dist/cjs/version.js +1 -1
  58. package/dist/esm/admin.d.ts +70 -0
  59. package/dist/esm/admin.d.ts.map +1 -1
  60. package/dist/esm/admin.js +72 -0
  61. package/dist/esm/admin.js.map +1 -1
  62. package/dist/esm/ai.d.ts +73 -0
  63. package/dist/esm/ai.d.ts.map +1 -0
  64. package/dist/esm/ai.js +116 -0
  65. package/dist/esm/ai.js.map +1 -0
  66. package/dist/esm/auth.d.ts +38 -0
  67. package/dist/esm/auth.d.ts.map +1 -1
  68. package/dist/esm/auth.js +52 -0
  69. package/dist/esm/auth.js.map +1 -1
  70. package/dist/esm/client.d.ts +18 -3
  71. package/dist/esm/client.d.ts.map +1 -1
  72. package/dist/esm/client.js +31 -4
  73. package/dist/esm/client.js.map +1 -1
  74. package/dist/esm/data.d.ts +28 -0
  75. package/dist/esm/data.d.ts.map +1 -1
  76. package/dist/esm/data.js +42 -7
  77. package/dist/esm/data.js.map +1 -1
  78. package/dist/esm/errors.d.ts +1 -1
  79. package/dist/esm/errors.d.ts.map +1 -1
  80. package/dist/esm/errors.js +8 -1
  81. package/dist/esm/errors.js.map +1 -1
  82. package/dist/esm/functions.d.ts +0 -28
  83. package/dist/esm/functions.d.ts.map +1 -1
  84. package/dist/esm/functions.js +0 -40
  85. package/dist/esm/functions.js.map +1 -1
  86. package/dist/esm/index.d.ts +4 -2
  87. package/dist/esm/index.d.ts.map +1 -1
  88. package/dist/esm/index.js +2 -1
  89. package/dist/esm/index.js.map +1 -1
  90. package/dist/esm/manage/access.d.ts +3 -3
  91. package/dist/esm/manage/access.d.ts.map +1 -1
  92. package/dist/esm/manage/access.js +3 -3
  93. package/dist/esm/manage/access.js.map +1 -1
  94. package/dist/esm/manage/ai.d.ts +90 -0
  95. package/dist/esm/manage/ai.d.ts.map +1 -0
  96. package/dist/esm/manage/ai.js +97 -0
  97. package/dist/esm/manage/ai.js.map +1 -0
  98. package/dist/esm/manage/data.d.ts +1 -1
  99. package/dist/esm/manage/data.d.ts.map +1 -1
  100. package/dist/esm/manage/data.js +1 -1
  101. package/dist/esm/manage/data.js.map +1 -1
  102. package/dist/esm/manage/index.d.ts +2 -3
  103. package/dist/esm/manage/index.d.ts.map +1 -1
  104. package/dist/esm/manage/index.js +4 -4
  105. package/dist/esm/manage/index.js.map +1 -1
  106. package/dist/esm/manage/mail.d.ts +1 -1
  107. package/dist/esm/manage/mail.d.ts.map +1 -1
  108. package/dist/esm/manage/mail.js +1 -1
  109. package/dist/esm/manage/mail.js.map +1 -1
  110. package/dist/esm/types.d.ts +3 -3
  111. package/dist/esm/types.d.ts.map +1 -1
  112. package/dist/esm/version.d.ts +1 -1
  113. package/dist/esm/version.js +1 -1
  114. package/package.json +1 -1
  115. package/src/admin.ts +104 -0
  116. package/src/ai.ts +155 -0
  117. package/src/auth.ts +70 -0
  118. package/src/client.ts +31 -4
  119. package/src/data.ts +52 -5
  120. package/src/errors.ts +6 -1
  121. package/src/functions.ts +0 -49
  122. package/src/index.ts +4 -1
  123. package/src/manage/access.ts +3 -3
  124. package/src/manage/ai.ts +129 -0
  125. package/src/manage/data.ts +1 -1
  126. package/src/manage/index.ts +4 -4
  127. package/src/manage/mail.ts +1 -1
  128. package/src/types.ts +3 -3
  129. package/src/version.ts +1 -1
  130. package/dist/cjs/manage/functions.d.ts +0 -48
  131. package/dist/cjs/manage/functions.d.ts.map +0 -1
  132. package/dist/cjs/manage/functions.js +0 -61
  133. package/dist/cjs/manage/functions.js.map +0 -1
  134. package/dist/esm/manage/functions.d.ts +0 -48
  135. package/dist/esm/manage/functions.d.ts.map +0 -1
  136. package/dist/esm/manage/functions.js +0 -57
  137. package/dist/esm/manage/functions.js.map +0 -1
  138. package/src/manage/functions.ts +0 -69
package/src/ai.ts ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The AI gateway: chat and embedding models your environment has connected, behind one OpenAI-compatible API.
3
+ *
4
+ * ```ts
5
+ * const answer = await pawabase.ai.chat("chat", "Summarise this ticket: …");
6
+ * for await (const piece of pawabase.ai.stream("chat", "Tell me a story")) process.stdout.write(piece);
7
+ * const vector = await pawabase.ai.embed("embed", "hello");
8
+ * ```
9
+ *
10
+ * Models are called by the name your environment gave them. Who may call each is the model's policy; servers (a secret key) always may.
11
+ */
12
+ import { ApiError } from "./errors.js";
13
+ import type { HttpClient } from "./http.js";
14
+
15
+ export interface ChatMessage {
16
+ role: "system" | "user" | "assistant" | "tool" | "developer";
17
+ content: string | null;
18
+ tool_calls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string } }>;
19
+ tool_call_id?: string;
20
+ name?: string;
21
+ }
22
+
23
+ export interface ChatOptions {
24
+ temperature?: number;
25
+ topP?: number;
26
+ maxTokens?: number;
27
+ stop?: string | string[];
28
+ /** Functions the model may ask you to call (OpenAI's `tools` format). */
29
+ tools?: unknown[];
30
+ toolChoice?: unknown;
31
+ /** `{ type: "json_object" }` asks for JSON back, where the provider supports it. */
32
+ responseFormat?: { type: string; [key: string]: unknown };
33
+ /** Any other parameter, passed through as given. */
34
+ extra?: Record<string, unknown>;
35
+ signal?: AbortSignal | null;
36
+ }
37
+
38
+ export interface ChatCompletion {
39
+ id: string;
40
+ object: "chat.completion";
41
+ model: string;
42
+ choices: Array<{ index: number; message: ChatMessage; finish_reason: string | null }>;
43
+ usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
44
+ }
45
+
46
+ export interface AiModelInfo {
47
+ id: string;
48
+ kind: "chat" | "embedding";
49
+ description: string;
50
+ dimensions?: number;
51
+ }
52
+
53
+ function asMessages(input: string | ChatMessage[]): ChatMessage[] {
54
+ return typeof input === "string" ? [{ role: "user", content: input }] : input;
55
+ }
56
+
57
+ function bodyOf(model: string, input: string | ChatMessage[], options: ChatOptions): Record<string, unknown> {
58
+ return {
59
+ ...(options.extra ?? {}),
60
+ model,
61
+ messages: asMessages(input),
62
+ ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
63
+ ...(options.topP !== undefined ? { top_p: options.topP } : {}),
64
+ ...(options.maxTokens !== undefined ? { max_tokens: options.maxTokens } : {}),
65
+ ...(options.stop !== undefined ? { stop: options.stop } : {}),
66
+ ...(options.tools !== undefined ? { tools: options.tools } : {}),
67
+ ...(options.toolChoice !== undefined ? { tool_choice: options.toolChoice } : {}),
68
+ ...(options.responseFormat !== undefined ? { response_format: options.responseFormat } : {}),
69
+ };
70
+ }
71
+
72
+ export class AiClient {
73
+ constructor(private readonly http: HttpClient) {}
74
+
75
+ /** The models the caller may use. */
76
+ async models(): Promise<AiModelInfo[]> {
77
+ const body = await this.http.request<{ data: AiModelInfo[] }>({ path: "/ai/v1/models" });
78
+ return body.data;
79
+ }
80
+
81
+ /** The full chat completion, in OpenAI's shape (tool calls, usage and all). */
82
+ complete(model: string, input: string | ChatMessage[], options: ChatOptions = {}): Promise<ChatCompletion> {
83
+ return this.http.request<ChatCompletion>({
84
+ method: "POST",
85
+ path: "/ai/v1/chat/completions",
86
+ body: bodyOf(model, input, options),
87
+ signal: options.signal ?? undefined,
88
+ });
89
+ }
90
+
91
+ /** Ask a model and get its answer as text. */
92
+ async chat(model: string, input: string | ChatMessage[], options: ChatOptions = {}): Promise<string> {
93
+ const completion = await this.complete(model, input, options);
94
+ return completion.choices[0]?.message?.content ?? "";
95
+ }
96
+
97
+ /** The answer as it is written: an async iterator of text pieces. */
98
+ async *stream(model: string, input: string | ChatMessage[], options: ChatOptions = {}): AsyncGenerator<string, void, void> {
99
+ const stream = await this.http.request<ReadableStream<Uint8Array> | null>({
100
+ method: "POST",
101
+ path: "/ai/v1/chat/completions",
102
+ body: { ...bodyOf(model, input, options), stream: true },
103
+ parse: "stream",
104
+ signal: options.signal ?? undefined,
105
+ });
106
+ if (!stream) return;
107
+ const reader = stream.getReader();
108
+ const decoder = new TextDecoder();
109
+ let buffer = "";
110
+ try {
111
+ for (;;) {
112
+ const { value, done } = await reader.read();
113
+ if (done) break;
114
+ buffer += decoder.decode(value, { stream: true });
115
+ let end: number;
116
+ while ((end = buffer.indexOf("\n\n")) >= 0) {
117
+ const event = buffer.slice(0, end);
118
+ buffer = buffer.slice(end + 2);
119
+ for (const line of event.split("\n")) {
120
+ if (!line.startsWith("data:")) continue;
121
+ const data = line.slice(5).trim();
122
+ if (data === "[DONE]") return;
123
+ let parsed: { choices?: Array<{ delta?: { content?: string } }>; error?: { message?: string; code?: string } };
124
+ try {
125
+ parsed = JSON.parse(data);
126
+ } catch {
127
+ continue;
128
+ }
129
+ if (parsed.error) {
130
+ throw new ApiError({ status: 502, code: parsed.error.code ?? "provider_error", message: parsed.error.message ?? "the model stopped answering" });
131
+ }
132
+ const piece = parsed.choices?.[0]?.delta?.content;
133
+ if (piece) yield piece;
134
+ }
135
+ }
136
+ }
137
+ } finally {
138
+ reader.releaseLock();
139
+ }
140
+ }
141
+
142
+ /** The vector for one text, or one vector per text when given a list. */
143
+ embed(model: string, input: string, options?: { signal?: AbortSignal | null }): Promise<number[]>;
144
+ embed(model: string, input: string[], options?: { signal?: AbortSignal | null }): Promise<number[][]>;
145
+ async embed(model: string, input: string | string[], options: { signal?: AbortSignal | null } = {}): Promise<number[] | number[][]> {
146
+ const body = await this.http.request<{ data: Array<{ index: number; embedding: number[] }> }>({
147
+ method: "POST",
148
+ path: "/ai/v1/embeddings",
149
+ body: { model, input },
150
+ signal: options.signal ?? undefined,
151
+ });
152
+ const vectors = [...body.data].sort((a, b) => a.index - b.index).map((row) => row.embedding);
153
+ return typeof input === "string" ? (vectors[0] ?? []) : vectors;
154
+ }
155
+ }
package/src/auth.ts CHANGED
@@ -149,6 +149,8 @@ export class AuthClient implements TokenProvider {
149
149
  readonly devices: AuthDevices;
150
150
  /** Keys a user makes for themselves to call the API as themselves. */
151
151
  readonly apiKeys: AuthApiKeys;
152
+ /** Sign a machine (a service or an AI agent) in with its client id and key. */
153
+ readonly machine: AuthMachine;
152
154
  /** Passkeys (WebAuthn): register one, sign in with one. */
153
155
  readonly passkeys: AuthPasskeys;
154
156
 
@@ -167,6 +169,7 @@ export class AuthClient implements TokenProvider {
167
169
  this.orgs = new AuthOrgs(http);
168
170
  this.devices = new AuthDevices(http);
169
171
  this.apiKeys = new AuthApiKeys(http);
172
+ this.machine = new AuthMachine(http);
170
173
  this.passkeys = new AuthPasskeys(http, (wire) => this.adopt(wire));
171
174
  this.ready = this.initialize();
172
175
  this.ready.catch(() => undefined);
@@ -747,6 +750,33 @@ export class AuthClient implements TokenProvider {
747
750
  return user;
748
751
  }
749
752
 
753
+ /**
754
+ * Set the signed-in user's picture from an image (PNG, JPEG, WebP or GIF). It is stored in the environment's public avatar
755
+ * bucket and `avatar_url` points at it; the previous picture is removed. Resolves the updated user.
756
+ */
757
+ async setAvatar(image: Blob | ArrayBuffer | ArrayBufferView, options: { contentType?: string } = {}): Promise<User> {
758
+ await this.ready;
759
+ const declared = options.contentType ?? (typeof Blob !== "undefined" && image instanceof Blob && image.type ? image.type : "");
760
+ await this.http.request({
761
+ method: "PUT",
762
+ path: "/auth/v1/user/avatar",
763
+ body: image,
764
+ headers: { "content-type": declared || "application/octet-stream" },
765
+ });
766
+ const user = await this.getUser();
767
+ this.emit("USER_UPDATED");
768
+ return user;
769
+ }
770
+
771
+ /** Remove the signed-in user's picture. */
772
+ async removeAvatar(): Promise<User> {
773
+ await this.ready;
774
+ await this.http.request({ method: "DELETE", path: "/auth/v1/user/avatar" });
775
+ const user = await this.getUser();
776
+ this.emit("USER_UPDATED");
777
+ return user;
778
+ }
779
+
750
780
  /** Send the confirmation email again. Always resolves the same way. */
751
781
  async resendVerification(input: { email: string; redirectTo?: string }): Promise<void> {
752
782
  await this.http.request({
@@ -1080,6 +1110,46 @@ class AuthDevices {
1080
1110
  }
1081
1111
  }
1082
1112
 
1113
+ /** What a machine gets for its credentials. */
1114
+ export interface MachineToken {
1115
+ accessToken: string;
1116
+ tokenType: "Bearer";
1117
+ /** Seconds the token lives. */
1118
+ expiresIn: number;
1119
+ /** The permissions the token carries, space-separated. */
1120
+ scope: string;
1121
+ machine: { id: string; name: string; kind: "service" | "agent" };
1122
+ }
1123
+
1124
+ class AuthMachine {
1125
+ constructor(private readonly http: HttpClient) {}
1126
+
1127
+ /**
1128
+ * The OAuth `client_credentials` grant: exchange a machine's id and one of its keys (`pbm_…`) for a short-lived access token.
1129
+ * `scope` (space-separated permissions) asks for fewer permissions than the key allows; it never grants more. Keeps no session.
1130
+ */
1131
+ async token(input: { clientId: string; clientSecret: string; scope?: string }): Promise<MachineToken> {
1132
+ const wire = await this.http.request<{
1133
+ access_token: string;
1134
+ token_type: "Bearer";
1135
+ expires_in: number;
1136
+ scope: string;
1137
+ machine: MachineToken["machine"];
1138
+ }>({
1139
+ method: "POST",
1140
+ path: "/auth/v1/token",
1141
+ body: {
1142
+ grant_type: "client_credentials",
1143
+ client_id: input.clientId,
1144
+ client_secret: input.clientSecret,
1145
+ ...(input.scope ? { scope: input.scope } : {}),
1146
+ },
1147
+ auth: "none",
1148
+ });
1149
+ return { accessToken: wire.access_token, tokenType: wire.token_type, expiresIn: wire.expires_in, scope: wire.scope, machine: wire.machine };
1150
+ }
1151
+ }
1152
+
1083
1153
  class AuthApiKeys {
1084
1154
  constructor(private readonly http: HttpClient) {}
1085
1155
 
package/src/client.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { AiClient } from "./ai.js";
1
2
  import { AuthClient, type AuthOptions } from "./auth.js";
2
3
  import { ResourceClient, type AnyRow, type ResourceTypes } from "./data.js";
3
4
  import { ConfigError } from "./errors.js";
4
5
  import { StorageClient } from "./files.js";
5
- import { FlowsClient, FunctionsClient, RoutesClient } from "./functions.js";
6
+ import { FlowsClient, RoutesClient } from "./functions.js";
6
7
  import { HttpClient, type FetchLike, type Hooks, type RetryOptions, type TokenProvider } from "./http.js";
7
8
  import { RealtimeClient, type RealtimeOptions } from "./realtime.js";
8
9
  import { MemoryStorage } from "./session-store.js";
@@ -72,8 +73,8 @@ export class PawabaseClient<DB extends DatabaseShape = Untyped> {
72
73
  readonly auth: AuthClient;
73
74
  /** Storage buckets. */
74
75
  readonly storage: StorageClient;
75
- /** Functions: `/functions/v1`. */
76
- readonly functions: FunctionsClient;
76
+ /** The AI gateway: chat and embedding models. */
77
+ readonly ai: AiClient;
77
78
  /** Flows with a pathless HTTP trigger: `/flows/v1`. */
78
79
  readonly flows: FlowsClient;
79
80
  /** Custom routes: `/rest/v1/<path>`. */
@@ -126,7 +127,7 @@ export class PawabaseClient<DB extends DatabaseShape = Untyped> {
126
127
  });
127
128
  config.tokens = stateless ? staticTokens(options.accessToken as NonNullable<ClientOptions["accessToken"]>) : this.auth;
128
129
  this.storage = new StorageClient(this.http);
129
- this.functions = new FunctionsClient(this.http);
130
+ this.ai = new AiClient(this.http);
130
131
  this.flows = new FlowsClient(this.http);
131
132
  this.routes = new RoutesClient(this.http);
132
133
  this.realtime = new RealtimeClient(this.http, stateless ? null : this.auth, {
@@ -156,6 +157,32 @@ export class PawabaseClient<DB extends DatabaseShape = Untyped> {
156
157
  return new PawabaseClient<DB>({ ...this.options, accessToken });
157
158
  }
158
159
 
160
+ /**
161
+ * A client that acts as a machine (a service or an AI agent). It signs in with the machine's id and key, keeps the token until shortly
162
+ * before it expires, and signs in again when it must. It keeps no user session.
163
+ *
164
+ * ```ts
165
+ * const agent = pawabase.asMachine({ clientId: process.env.PB_MACHINE_ID!, clientSecret: process.env.PB_MACHINE_KEY! });
166
+ * await agent.from("orders").eq("status", "open");
167
+ * ```
168
+ */
169
+ asMachine(input: { clientId: string; clientSecret: string; scope?: string }): PawabaseClient<DB> {
170
+ let cached: { token: string; until: number } | null = null;
171
+ let pending: Promise<string> | null = null;
172
+ const fetchToken = async (): Promise<string> => {
173
+ const got = await this.auth.machine.token(input);
174
+ cached = { token: got.accessToken, until: Date.now() + Math.max(got.expiresIn - 30, 1) * 1000 };
175
+ return got.accessToken;
176
+ };
177
+ return this.asUser(async () => {
178
+ if (cached && cached.until > Date.now()) return cached.token;
179
+ pending ??= fetchToken().finally(() => {
180
+ pending = null;
181
+ });
182
+ return pending;
183
+ });
184
+ }
185
+
159
186
  /**
160
187
  * A raw request to any path on the gateway, with the key and the user's token attached,
161
188
  * the retry policy and the typed errors applied. For endpoints the client has no method for.
package/src/data.ts CHANGED
@@ -47,6 +47,8 @@ interface State {
47
47
  expand: string[];
48
48
  page: number;
49
49
  perPage: number | null;
50
+ /** False asks for every match in one response. */
51
+ paginate: boolean;
50
52
  /** An `in ()` with no values matches nothing: answered locally. */
51
53
  empty: boolean;
52
54
  }
@@ -121,6 +123,7 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
121
123
  expand: [],
122
124
  page: 1,
123
125
  perPage: null,
126
+ paginate: true,
124
127
  empty: false,
125
128
  },
126
129
  private readonly options: QueryOptions = {},
@@ -248,6 +251,14 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
248
251
  return this.with({ perPage });
249
252
  }
250
253
 
254
+ /**
255
+ * Turn paging off with `paginate(false)`: the response holds every match (up to 5000; the server refuses more, so narrow
256
+ * with filters). `page` and `per_page` come back `null`.
257
+ */
258
+ paginate(paginate = true): QueryBuilder<Row> {
259
+ return this.with({ paginate });
260
+ }
261
+
251
262
  /** Alias of {@link perPage}. */
252
263
  limit(count: number): QueryBuilder<Row> {
253
264
  return this.perPage(count);
@@ -266,15 +277,20 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
266
277
  if (this.state.sort.length) params["sort"] = this.state.sort.join(",");
267
278
  if (this.state.select) params["select"] = this.state.select.join(",");
268
279
  if (this.state.expand.length) params["expand"] = [...new Set(this.state.expand)].join(",");
269
- if (this.state.page !== 1) params["page"] = this.state.page;
270
- if (this.state.perPage !== null) params["per_page"] = this.state.perPage;
280
+ if (!this.state.paginate) params["paginate"] = "false";
281
+ else {
282
+ if (this.state.page !== 1) params["page"] = this.state.page;
283
+ if (this.state.perPage !== null) params["per_page"] = this.state.perPage;
284
+ }
271
285
  return params;
272
286
  }
273
287
 
274
288
  /** Run the query and return one page. `await`ing the builder does the same. */
275
289
  async execute(): Promise<Page<Row>> {
276
290
  if (this.state.empty) {
277
- return { data: [], page: this.state.page, per_page: this.state.perPage ?? 20, total: 0 };
291
+ return this.state.paginate
292
+ ? { data: [], page: this.state.page, per_page: this.state.perPage ?? 20, total: 0 }
293
+ : { data: [], page: null, per_page: null, total: 0 };
278
294
  }
279
295
  return this.http.request<Page<Row>>({
280
296
  path: `/rest/v1/${encodePath(this.resource)}`,
@@ -341,13 +357,13 @@ export class QueryBuilder<Row = AnyRow> implements PromiseLike<Page<Row>> {
341
357
  seen += page.data.length;
342
358
  if (options.maxRecords !== undefined && seen >= options.maxRecords) return;
343
359
  if (page.total !== null) {
344
- if (page.page * page.per_page >= page.total) return;
360
+ if ((page.page ?? 1) * (page.per_page ?? 0) >= page.total) return;
345
361
  } else {
346
362
  // Unknown total: a per-row policy filters after the page is read, so a short page proves nothing.
347
363
  empties = page.data.length === 0 ? empties + 1 : 0;
348
364
  if (empties >= stopAfterEmpty) return;
349
365
  }
350
- query = query.page(page.page + 1);
366
+ query = query.page((page.page ?? 1) + 1);
351
367
  }
352
368
  }
353
369
 
@@ -392,6 +408,34 @@ export class ResourceClient<
392
408
  return id === undefined ? base : `${base}/${encodeURIComponent(String(id))}`;
393
409
  }
394
410
 
411
+ /**
412
+ * Records closest in meaning, closest first, each with a `distance` (smaller is closer). Give `text` (turned into a vector by the
413
+ * field's embedding model) or a `vector`. Vector fields are left out of the records unless you ask for them.
414
+ */
415
+ async similar(input: {
416
+ text?: string;
417
+ vector?: number[];
418
+ field?: string;
419
+ metric?: "cosine" | "l2" | "ip";
420
+ limit?: number;
421
+ maxDistance?: number;
422
+ signal?: AbortSignal | null;
423
+ }): Promise<{ data: Array<Row & { distance: number }>; metric: string; count: number }> {
424
+ return this.http.request({
425
+ method: "POST",
426
+ path: `${this.path()}/similar`,
427
+ body: {
428
+ ...(input.text !== undefined ? { text: input.text } : {}),
429
+ ...(input.vector !== undefined ? { vector: input.vector } : {}),
430
+ ...(input.field ? { field: input.field } : {}),
431
+ metric: input.metric ?? "cosine",
432
+ limit: input.limit ?? 10,
433
+ ...(input.maxDistance !== undefined ? { max_distance: input.maxDistance } : {}),
434
+ },
435
+ signal: input.signal ?? undefined,
436
+ });
437
+ }
438
+
395
439
  /** Start a query. Every filter and shape method below is a shortcut for `query().…`. */
396
440
  query(): QueryBuilder<Row> {
397
441
  return new QueryBuilder<Row>(this.http, this.name);
@@ -405,6 +449,8 @@ export class ResourceClient<
405
449
  expand?: string[];
406
450
  page?: number;
407
451
  perPage?: number;
452
+ /** `false` returns every match in one response (up to 5000). */
453
+ paginate?: boolean;
408
454
  signal?: AbortSignal | null;
409
455
  } = {}): Promise<Page<Row>> {
410
456
  let query = this.query();
@@ -417,6 +463,7 @@ export class ResourceClient<
417
463
  if (params.expand) query = query.expand(...params.expand);
418
464
  if (params.page) query = query.page(params.page);
419
465
  if (params.perPage) query = query.perPage(params.perPage);
466
+ if (params.paginate === false) query = query.paginate(false);
420
467
  if (params.signal) query = query.withOptions({ signal: params.signal });
421
468
  return query.execute();
422
469
  }
package/src/errors.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Pawabase answers errors in several shapes: a bare string (most `4xx`), a bare
5
5
  * array of validation issues (`422`), an `{error, message, details}` object
6
- * (functions, flows, the gateway) and Akountz's own `{detail}` bodies. The
6
+ * (flows, the gateway) and Akountz's own `{detail}` bodies. The
7
7
  * client normalises all of them into one class hierarchy, so application code
8
8
  * checks `error.status`, `error.code` or `instanceof` and never parses a body.
9
9
  */
@@ -277,6 +277,11 @@ export function errorFromResponse(input: {
277
277
  else if (typeof body["code"] === "string") code = body["code"] as string;
278
278
  if (typeof body["message"] === "string") message = body["message"] as string;
279
279
  else if (typeof detail === "string") message = detail;
280
+ // OpenAI-style bodies, which the AI gateway answers with: { error: { message, type, code } }.
281
+ if (isRecord(named)) {
282
+ if (!message && typeof named["message"] === "string") message = named["message"] as string;
283
+ if (typeof named["code"] === "string") code = named["code"] as string;
284
+ }
280
285
  if (Array.isArray(detail)) {
281
286
  issues = toIssues(detail);
282
287
  message ||= summarise(issues);
package/src/functions.ts CHANGED
@@ -24,55 +24,6 @@ function pass(options: CallOptions): {
24
24
  };
25
25
  }
26
26
 
27
- export interface FetchOptions extends CallOptions {
28
- method?: string;
29
- query?: Record<string, QueryValue | QueryValue[]>;
30
- /** An object is sent as JSON; strings, `Blob`, `FormData`, buffers and streams as they are. */
31
- body?: unknown;
32
- /** Run the function deployed on this branch (`x-pawabase-branch`). */
33
- branch?: string;
34
- }
35
-
36
- /** Functions you wrote in your project's code, invoked at `/functions/v1/<name>`. */
37
- export class FunctionsClient {
38
- constructor(private readonly http: HttpClient) {}
39
-
40
- /**
41
- * Call a function and return what it returned. The API key needs the
42
- * `functions:invoke` scope (an unrestricted key has it).
43
- */
44
- async invoke<T = unknown>(name: string, input?: unknown, options: CallOptions = {}): Promise<T> {
45
- const body = await this.http.request<{ data: T } | undefined>({
46
- method: "POST",
47
- path: `/functions/v1/${encodePath(name)}`,
48
- body: input === undefined ? undefined : input,
49
- ...pass(options),
50
- });
51
- return (body as { data: T } | undefined)?.data as T;
52
- }
53
-
54
- /**
55
- * Call an HTTP function (`functions/<name>/index.ts`) with any method and path, and get its `Response` as it is — status, headers
56
- * and a streaming body. An error status is returned, not thrown; the API key and the signed-in user's token are sent as for any call.
57
- *
58
- * ```ts
59
- * const res = await pawabase.functions.fetch("orders", "/42/items", { method: "POST", body: { sku: "A1" } });
60
- * if (res.ok) console.log(await res.json());
61
- * ```
62
- */
63
- fetch(name: string, path = "/", options: FetchOptions = {}): Promise<Response> {
64
- const rest = path === "/" || path === "" ? "" : path.startsWith("/") ? path : `/${path}`;
65
- return this.http.send({
66
- method: options.method ?? (options.body === undefined ? "GET" : "POST"),
67
- path: `/functions/v1/${encodePath(name)}${rest}`,
68
- ...(options.query ? { query: options.query } : {}),
69
- ...(options.body !== undefined ? { body: options.body } : {}),
70
- ...pass({ ...options, headers: { ...(options.branch ? { "x-pawabase-branch": options.branch } : {}), ...options.headers } }),
71
- throwOnError: false,
72
- });
73
- }
74
- }
75
-
76
27
  /**
77
28
  * Flows with an HTTP trigger that has no path, invoked at `/flows/v1/<name>`.
78
29
  * (Flows bound to a path are reached as routes, see {@link RoutesClient}.)
package/src/index.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  export { PawabaseClient, createClient } from "./client.js";
2
2
  export type { ClientOptions, DatabaseShape } from "./client.js";
3
3
 
4
+ export { AiClient } from "./ai.js";
5
+ export type { AiModelInfo, ChatCompletion, ChatMessage, ChatOptions } from "./ai.js";
4
6
  export { AuthClient } from "./auth.js";
5
7
  export type {
6
8
  AuthChangeEvent,
7
9
  AuthListener,
8
10
  AuthOptions,
9
11
  IssuedToken,
12
+ MachineToken,
10
13
  Passkey,
11
14
  PasskeyChallenge,
12
15
  SignInResult,
@@ -29,7 +32,7 @@ export type {
29
32
  ResourceTypes,
30
33
  } from "./data.js";
31
34
 
32
- export { FlowsClient, FunctionsClient, RoutesClient } from "./functions.js";
35
+ export { FlowsClient, RoutesClient } from "./functions.js";
33
36
  export type { CallOptions, RouteOptions } from "./functions.js";
34
37
 
35
38
  export { BucketClient, StorageClient } from "./files.js";
@@ -15,8 +15,8 @@ export class Keys extends Service {
15
15
  }
16
16
 
17
17
  /**
18
- * Make a key; the key itself is in the answer once. `scopes` limit what it may do (`resource:read`, `functions:invoke`…),
19
- * `allowedIps` and `allowedRoutes` (`"GET /rest/v1/orders"`, `"/functions/v1/*"`) limit where and what it may call.
18
+ * Make a key; the key itself is in the answer once. `scopes` limit what it may do (`resource:read`, `routes:invoke`…),
19
+ * `allowedIps` and `allowedRoutes` (`"GET /rest/v1/orders"`, `"/flows/v1/*"`) limit where and what it may call.
20
20
  */
21
21
  create(name: string, options: { role?: "publishable" | "secret"; scopes?: string[]; expiresAt?: When; allowedIps?: string[]; allowedRoutes?: string[] } = {}): Promise<Api> {
22
22
  return this.post("/keys", {
@@ -39,7 +39,7 @@ export class Keys extends Service {
39
39
  }
40
40
  }
41
41
 
42
- /** Secrets functions and flows read (`ctx.runtime.secret(name)`). Values are never returned. */
42
+ /** Secrets flows read (`ctx.runtime.secret(name)`). Values are never returned. */
43
43
  export class Secrets extends Service {
44
44
  list(): Promise<Api> {
45
45
  return this.get("/secrets");