@pawabase/client 0.0.0-stage → 0.1.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.
Files changed (243) hide show
  1. package/README.md +313 -2
  2. package/dist/cjs/admin.d.ts +187 -0
  3. package/dist/cjs/admin.d.ts.map +1 -0
  4. package/dist/cjs/admin.js +235 -0
  5. package/dist/cjs/admin.js.map +1 -0
  6. package/dist/cjs/auth.d.ts +448 -0
  7. package/dist/cjs/auth.d.ts.map +1 -0
  8. package/dist/cjs/auth.js +1016 -0
  9. package/dist/cjs/auth.js.map +1 -0
  10. package/dist/cjs/client.d.ts +103 -0
  11. package/dist/cjs/client.d.ts.map +1 -0
  12. package/dist/cjs/client.js +144 -0
  13. package/dist/cjs/client.js.map +1 -0
  14. package/dist/cjs/data.d.ts +220 -0
  15. package/dist/cjs/data.d.ts.map +1 -0
  16. package/dist/cjs/data.js +490 -0
  17. package/dist/cjs/data.js.map +1 -0
  18. package/dist/cjs/errors.d.ts +167 -0
  19. package/dist/cjs/errors.d.ts.map +1 -0
  20. package/dist/cjs/errors.js +316 -0
  21. package/dist/cjs/errors.js.map +1 -0
  22. package/dist/cjs/files.d.ts +80 -0
  23. package/dist/cjs/files.d.ts.map +1 -0
  24. package/dist/cjs/files.js +167 -0
  25. package/dist/cjs/files.js.map +1 -0
  26. package/dist/cjs/functions.d.ts +66 -0
  27. package/dist/cjs/functions.d.ts.map +1 -0
  28. package/dist/cjs/functions.js +109 -0
  29. package/dist/cjs/functions.js.map +1 -0
  30. package/dist/cjs/http.d.ts +101 -0
  31. package/dist/cjs/http.d.ts.map +1 -0
  32. package/dist/cjs/http.js +249 -0
  33. package/dist/cjs/http.js.map +1 -0
  34. package/dist/cjs/index.d.ts +22 -0
  35. package/dist/cjs/index.d.ts.map +1 -0
  36. package/dist/cjs/index.js +45 -0
  37. package/dist/cjs/index.js.map +1 -0
  38. package/dist/cjs/manage/access.d.ts +136 -0
  39. package/dist/cjs/manage/access.d.ts.map +1 -0
  40. package/dist/cjs/manage/access.js +162 -0
  41. package/dist/cjs/manage/access.js.map +1 -0
  42. package/dist/cjs/manage/automation.d.ts +106 -0
  43. package/dist/cjs/manage/automation.d.ts.map +1 -0
  44. package/dist/cjs/manage/automation.js +144 -0
  45. package/dist/cjs/manage/automation.js.map +1 -0
  46. package/dist/cjs/manage/base.d.ts +52 -0
  47. package/dist/cjs/manage/base.d.ts.map +1 -0
  48. package/dist/cjs/manage/base.js +69 -0
  49. package/dist/cjs/manage/base.js.map +1 -0
  50. package/dist/cjs/manage/data.d.ts +100 -0
  51. package/dist/cjs/manage/data.d.ts.map +1 -0
  52. package/dist/cjs/manage/data.js +141 -0
  53. package/dist/cjs/manage/data.js.map +1 -0
  54. package/dist/cjs/manage/files.d.ts +45 -0
  55. package/dist/cjs/manage/files.d.ts.map +1 -0
  56. package/dist/cjs/manage/files.js +73 -0
  57. package/dist/cjs/manage/files.js.map +1 -0
  58. package/dist/cjs/manage/functions.d.ts +48 -0
  59. package/dist/cjs/manage/functions.d.ts.map +1 -0
  60. package/dist/cjs/manage/functions.js +61 -0
  61. package/dist/cjs/manage/functions.js.map +1 -0
  62. package/dist/cjs/manage/index.d.ts +76 -0
  63. package/dist/cjs/manage/index.d.ts.map +1 -0
  64. package/dist/cjs/manage/index.js +134 -0
  65. package/dist/cjs/manage/index.js.map +1 -0
  66. package/dist/cjs/manage/lifecycle.d.ts +128 -0
  67. package/dist/cjs/manage/lifecycle.d.ts.map +1 -0
  68. package/dist/cjs/manage/lifecycle.js +167 -0
  69. package/dist/cjs/manage/lifecycle.js.map +1 -0
  70. package/dist/cjs/manage/mail.d.ts +16 -0
  71. package/dist/cjs/manage/mail.d.ts.map +1 -0
  72. package/dist/cjs/manage/mail.js +24 -0
  73. package/dist/cjs/manage/mail.js.map +1 -0
  74. package/dist/cjs/manage/observe.d.ts +149 -0
  75. package/dist/cjs/manage/observe.d.ts.map +1 -0
  76. package/dist/cjs/manage/observe.js +168 -0
  77. package/dist/cjs/manage/observe.js.map +1 -0
  78. package/dist/cjs/manage/realtime.d.ts +52 -0
  79. package/dist/cjs/manage/realtime.d.ts.map +1 -0
  80. package/dist/cjs/manage/realtime.js +71 -0
  81. package/dist/cjs/manage/realtime.js.map +1 -0
  82. package/dist/cjs/package.json +1 -0
  83. package/dist/cjs/realtime.d.ts +207 -0
  84. package/dist/cjs/realtime.d.ts.map +1 -0
  85. package/dist/cjs/realtime.js +704 -0
  86. package/dist/cjs/realtime.js.map +1 -0
  87. package/dist/cjs/session-store.d.ts +16 -0
  88. package/dist/cjs/session-store.d.ts.map +1 -0
  89. package/dist/cjs/session-store.js +37 -0
  90. package/dist/cjs/session-store.js.map +1 -0
  91. package/dist/cjs/typegen.d.ts +26 -0
  92. package/dist/cjs/typegen.d.ts.map +1 -0
  93. package/dist/cjs/typegen.js +167 -0
  94. package/dist/cjs/typegen.js.map +1 -0
  95. package/dist/cjs/types.d.ts +178 -0
  96. package/dist/cjs/types.d.ts.map +1 -0
  97. package/dist/cjs/types.js +4 -0
  98. package/dist/cjs/types.js.map +1 -0
  99. package/dist/cjs/util.d.ts +27 -0
  100. package/dist/cjs/util.d.ts.map +1 -0
  101. package/dist/cjs/util.js +115 -0
  102. package/dist/cjs/util.js.map +1 -0
  103. package/dist/cjs/version.d.ts +2 -0
  104. package/dist/cjs/version.d.ts.map +1 -0
  105. package/dist/cjs/version.js +5 -0
  106. package/dist/cjs/version.js.map +1 -0
  107. package/dist/esm/admin.d.ts +187 -0
  108. package/dist/esm/admin.d.ts.map +1 -0
  109. package/dist/esm/admin.js +222 -0
  110. package/dist/esm/admin.js.map +1 -0
  111. package/dist/esm/auth.d.ts +448 -0
  112. package/dist/esm/auth.d.ts.map +1 -0
  113. package/dist/esm/auth.js +1012 -0
  114. package/dist/esm/auth.js.map +1 -0
  115. package/dist/esm/cli/gen-types.d.ts +2 -0
  116. package/dist/esm/cli/gen-types.d.ts.map +1 -0
  117. package/dist/esm/cli/gen-types.js +100 -0
  118. package/dist/esm/cli/gen-types.js.map +1 -0
  119. package/dist/esm/client.d.ts +103 -0
  120. package/dist/esm/client.d.ts.map +1 -0
  121. package/dist/esm/client.js +139 -0
  122. package/dist/esm/client.js.map +1 -0
  123. package/dist/esm/data.d.ts +220 -0
  124. package/dist/esm/data.d.ts.map +1 -0
  125. package/dist/esm/data.js +485 -0
  126. package/dist/esm/data.js.map +1 -0
  127. package/dist/esm/errors.d.ts +167 -0
  128. package/dist/esm/errors.d.ts.map +1 -0
  129. package/dist/esm/errors.js +293 -0
  130. package/dist/esm/errors.js.map +1 -0
  131. package/dist/esm/files.d.ts +80 -0
  132. package/dist/esm/files.d.ts.map +1 -0
  133. package/dist/esm/files.js +162 -0
  134. package/dist/esm/files.js.map +1 -0
  135. package/dist/esm/functions.d.ts +66 -0
  136. package/dist/esm/functions.d.ts.map +1 -0
  137. package/dist/esm/functions.js +103 -0
  138. package/dist/esm/functions.js.map +1 -0
  139. package/dist/esm/http.d.ts +101 -0
  140. package/dist/esm/http.d.ts.map +1 -0
  141. package/dist/esm/http.js +245 -0
  142. package/dist/esm/http.js.map +1 -0
  143. package/dist/esm/index.d.ts +22 -0
  144. package/dist/esm/index.d.ts.map +1 -0
  145. package/dist/esm/index.js +12 -0
  146. package/dist/esm/index.js.map +1 -0
  147. package/dist/esm/manage/access.d.ts +136 -0
  148. package/dist/esm/manage/access.d.ts.map +1 -0
  149. package/dist/esm/manage/access.js +155 -0
  150. package/dist/esm/manage/access.js.map +1 -0
  151. package/dist/esm/manage/automation.d.ts +106 -0
  152. package/dist/esm/manage/automation.d.ts.map +1 -0
  153. package/dist/esm/manage/automation.js +134 -0
  154. package/dist/esm/manage/automation.js.map +1 -0
  155. package/dist/esm/manage/base.d.ts +52 -0
  156. package/dist/esm/manage/base.d.ts.map +1 -0
  157. package/dist/esm/manage/base.js +63 -0
  158. package/dist/esm/manage/base.js.map +1 -0
  159. package/dist/esm/manage/data.d.ts +100 -0
  160. package/dist/esm/manage/data.d.ts.map +1 -0
  161. package/dist/esm/manage/data.js +130 -0
  162. package/dist/esm/manage/data.js.map +1 -0
  163. package/dist/esm/manage/files.d.ts +45 -0
  164. package/dist/esm/manage/files.d.ts.map +1 -0
  165. package/dist/esm/manage/files.js +68 -0
  166. package/dist/esm/manage/files.js.map +1 -0
  167. package/dist/esm/manage/functions.d.ts +48 -0
  168. package/dist/esm/manage/functions.d.ts.map +1 -0
  169. package/dist/esm/manage/functions.js +57 -0
  170. package/dist/esm/manage/functions.js.map +1 -0
  171. package/dist/esm/manage/index.d.ts +76 -0
  172. package/dist/esm/manage/index.d.ts.map +1 -0
  173. package/dist/esm/manage/index.js +115 -0
  174. package/dist/esm/manage/index.js.map +1 -0
  175. package/dist/esm/manage/lifecycle.d.ts +128 -0
  176. package/dist/esm/manage/lifecycle.d.ts.map +1 -0
  177. package/dist/esm/manage/lifecycle.js +161 -0
  178. package/dist/esm/manage/lifecycle.js.map +1 -0
  179. package/dist/esm/manage/mail.d.ts +16 -0
  180. package/dist/esm/manage/mail.d.ts.map +1 -0
  181. package/dist/esm/manage/mail.js +19 -0
  182. package/dist/esm/manage/mail.js.map +1 -0
  183. package/dist/esm/manage/observe.d.ts +149 -0
  184. package/dist/esm/manage/observe.d.ts.map +1 -0
  185. package/dist/esm/manage/observe.js +161 -0
  186. package/dist/esm/manage/observe.js.map +1 -0
  187. package/dist/esm/manage/realtime.d.ts +52 -0
  188. package/dist/esm/manage/realtime.d.ts.map +1 -0
  189. package/dist/esm/manage/realtime.js +66 -0
  190. package/dist/esm/manage/realtime.js.map +1 -0
  191. package/dist/esm/package.json +1 -0
  192. package/dist/esm/realtime.d.ts +207 -0
  193. package/dist/esm/realtime.d.ts.map +1 -0
  194. package/dist/esm/realtime.js +699 -0
  195. package/dist/esm/realtime.js.map +1 -0
  196. package/dist/esm/session-store.d.ts +16 -0
  197. package/dist/esm/session-store.d.ts.map +1 -0
  198. package/dist/esm/session-store.js +32 -0
  199. package/dist/esm/session-store.js.map +1 -0
  200. package/dist/esm/typegen.d.ts +26 -0
  201. package/dist/esm/typegen.d.ts.map +1 -0
  202. package/dist/esm/typegen.js +163 -0
  203. package/dist/esm/typegen.js.map +1 -0
  204. package/dist/esm/types.d.ts +178 -0
  205. package/dist/esm/types.d.ts.map +1 -0
  206. package/dist/esm/types.js +3 -0
  207. package/dist/esm/types.js.map +1 -0
  208. package/dist/esm/util.d.ts +27 -0
  209. package/dist/esm/util.d.ts.map +1 -0
  210. package/dist/esm/util.js +102 -0
  211. package/dist/esm/util.js.map +1 -0
  212. package/dist/esm/version.d.ts +2 -0
  213. package/dist/esm/version.d.ts.map +1 -0
  214. package/dist/esm/version.js +2 -0
  215. package/dist/esm/version.js.map +1 -0
  216. package/package.json +84 -4
  217. package/src/admin.ts +311 -0
  218. package/src/auth.ts +1180 -0
  219. package/src/cli/gen-types.ts +105 -0
  220. package/src/client.ts +189 -0
  221. package/src/data.ts +576 -0
  222. package/src/errors.ts +330 -0
  223. package/src/files.ts +208 -0
  224. package/src/functions.ts +131 -0
  225. package/src/http.ts +354 -0
  226. package/src/index.ts +56 -0
  227. package/src/manage/access.ts +206 -0
  228. package/src/manage/automation.ts +162 -0
  229. package/src/manage/base.ts +83 -0
  230. package/src/manage/data.ts +159 -0
  231. package/src/manage/files.ts +79 -0
  232. package/src/manage/functions.ts +69 -0
  233. package/src/manage/index.ts +132 -0
  234. package/src/manage/lifecycle.ts +208 -0
  235. package/src/manage/mail.ts +22 -0
  236. package/src/manage/observe.ts +226 -0
  237. package/src/manage/realtime.ts +95 -0
  238. package/src/realtime.ts +765 -0
  239. package/src/session-store.ts +36 -0
  240. package/src/typegen.ts +192 -0
  241. package/src/types.ts +195 -0
  242. package/src/util.ts +114 -0
  243. package/src/version.ts +1 -0
package/src/errors.ts ADDED
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Every failure the client raises is a {@link PawabaseError}.
3
+ *
4
+ * Pawabase answers errors in several shapes: a bare string (most `4xx`), a bare
5
+ * array of validation issues (`422`), an `{error, message, details}` object
6
+ * (functions, flows, the gateway) and Akountz's own `{detail}` bodies. The
7
+ * client normalises all of them into one class hierarchy, so application code
8
+ * checks `error.status`, `error.code` or `instanceof` and never parses a body.
9
+ */
10
+
11
+ /** One field a request body was refused for (`422`). */
12
+ export interface ValidationIssue {
13
+ /** Path to the field, e.g. `["body", "email"]` or `["query", "per_page"]`. */
14
+ loc: Array<string | number>;
15
+ /** The field name without the `body`/`query` prefix, e.g. `email`. */
16
+ field: string;
17
+ message: string;
18
+ type?: string;
19
+ input?: unknown;
20
+ }
21
+
22
+ export interface ApiErrorInit {
23
+ status: number;
24
+ code: string;
25
+ message: string;
26
+ details?: unknown;
27
+ body?: unknown;
28
+ requestId?: string | null;
29
+ method?: string;
30
+ url?: string;
31
+ retryAfter?: number | null;
32
+ policy?: string | null;
33
+ cause?: unknown;
34
+ }
35
+
36
+ /** The base class of everything this package throws. */
37
+ export class PawabaseError extends Error {
38
+ override name = "PawabaseError";
39
+ constructor(message: string, options?: { cause?: unknown }) {
40
+ super(message, options as ErrorOptions);
41
+ Object.setPrototypeOf(this, new.target.prototype);
42
+ }
43
+ }
44
+
45
+ /** The server answered with an error status. */
46
+ export class ApiError extends PawabaseError {
47
+ override name = "ApiError";
48
+ /** HTTP status code. */
49
+ readonly status: number;
50
+ /**
51
+ * A stable machine-readable code: the server's own (`rate_limit_exceeded`,
52
+ * `invalid_api_key`, a flow's `raised` code…) or one derived from the status
53
+ * (`unauthenticated`, `forbidden`, `not_found`, `conflict`, `validation_failed`,
54
+ * `rate_limited`, `server_error`).
55
+ */
56
+ readonly code: string;
57
+ /** Structured extras the server sent (validation issues, flow details). */
58
+ readonly details: unknown;
59
+ /** The parsed response body exactly as received. */
60
+ readonly body: unknown;
61
+ /** The `x-request-id` to quote to an operator; look it up in Studio → Observability. */
62
+ readonly requestId: string | null;
63
+ readonly method: string | undefined;
64
+ readonly url: string | undefined;
65
+ /** Seconds the server asked the caller to wait (`429`, `503`). */
66
+ readonly retryAfter: number | null;
67
+ /** The name of the policy that refused the request (`policy 'x' refused`), when stated. */
68
+ readonly policy: string | null;
69
+
70
+ constructor(init: ApiErrorInit) {
71
+ super(init.message, { cause: init.cause });
72
+ this.status = init.status;
73
+ this.code = init.code;
74
+ this.details = init.details;
75
+ this.body = init.body;
76
+ this.requestId = init.requestId ?? null;
77
+ this.method = init.method;
78
+ this.url = init.url;
79
+ this.retryAfter = init.retryAfter ?? null;
80
+ this.policy = init.policy ?? null;
81
+ }
82
+
83
+ /** True when `code` matches. Handy in `catch`: `if (e instanceof ApiError && e.is("not_found"))`. */
84
+ is(code: string): boolean {
85
+ return this.code === code;
86
+ }
87
+ }
88
+
89
+ /** `401`: no or invalid credentials. The API key is wrong, or the user is not signed in (or the token expired). */
90
+ export class AuthenticationError extends ApiError {
91
+ override name = "AuthenticationError";
92
+ }
93
+
94
+ /** `403`: signed in (or keyed) but not allowed. {@link ApiError.policy} names the refusing policy when stated. */
95
+ export class PermissionError extends ApiError {
96
+ override name = "PermissionError";
97
+ }
98
+
99
+ /** `404`: no such record, route or file, or a read policy hides it. */
100
+ export class NotFoundError extends ApiError {
101
+ override name = "NotFoundError";
102
+ }
103
+
104
+ /** `409`: a unique value is taken, or the request conflicts with current state. */
105
+ export class ConflictError extends ApiError {
106
+ override name = "ConflictError";
107
+ }
108
+
109
+ /** `400`/`422`: the request was refused before any policy ran. See {@link issues}. */
110
+ export class ValidationError extends ApiError {
111
+ override name = "ValidationError";
112
+ readonly issues: ValidationIssue[];
113
+ constructor(init: ApiErrorInit & { issues: ValidationIssue[] }) {
114
+ super(init);
115
+ this.issues = init.issues;
116
+ }
117
+ /** The first message for a field, or `undefined`. Useful for form errors. */
118
+ fieldError(field: string): string | undefined {
119
+ return this.issues.find((issue) => issue.field === field)?.message;
120
+ }
121
+ /** All issues by field: `{ email: ["must be a valid email"], … }`. */
122
+ byField(): Record<string, string[]> {
123
+ const out: Record<string, string[]> = {};
124
+ for (const issue of this.issues) (out[issue.field] ??= []).push(issue.message);
125
+ return out;
126
+ }
127
+ }
128
+
129
+ /** `413`: the body is larger than the gateway or bucket allows. */
130
+ export class PayloadTooLargeError extends ApiError {
131
+ override name = "PayloadTooLargeError";
132
+ }
133
+
134
+ /** `429`: rate limited. {@link ApiError.retryAfter} is the wait in seconds. */
135
+ export class RateLimitError extends ApiError {
136
+ override name = "RateLimitError";
137
+ }
138
+
139
+ /** `5xx`: the platform failed. `502`/`503`/`504` are retried automatically for safe requests. */
140
+ export class ServerError extends ApiError {
141
+ override name = "ServerError";
142
+ }
143
+
144
+ /** The request never produced a response (offline, DNS, refused, reset). */
145
+ export class NetworkError extends PawabaseError {
146
+ override name = "NetworkError";
147
+ readonly method: string | undefined;
148
+ readonly url: string | undefined;
149
+ constructor(message: string, options?: { cause?: unknown; method?: string; url?: string }) {
150
+ super(message, options);
151
+ this.method = options?.method;
152
+ this.url = options?.url;
153
+ }
154
+ }
155
+
156
+ /** The request exceeded its `timeoutMs`. */
157
+ export class TimeoutError extends PawabaseError {
158
+ override name = "TimeoutError";
159
+ readonly timeoutMs: number;
160
+ constructor(timeoutMs: number, options?: { cause?: unknown }) {
161
+ super(`The request timed out after ${timeoutMs} ms.`, options);
162
+ this.timeoutMs = timeoutMs;
163
+ }
164
+ }
165
+
166
+ /** The caller's `AbortSignal` fired. */
167
+ export class AbortError extends PawabaseError {
168
+ override name = "AbortError";
169
+ constructor(message = "The request was aborted.", options?: { cause?: unknown }) {
170
+ super(message, options);
171
+ }
172
+ }
173
+
174
+ /** The client was configured or used incorrectly (no URL, a secret key in a browser…). */
175
+ export class ConfigError extends PawabaseError {
176
+ override name = "ConfigError";
177
+ }
178
+
179
+ /** A realtime operation failed: refused, not connected, or no acknowledgement. */
180
+ export class RealtimeError extends PawabaseError {
181
+ override name = "RealtimeError";
182
+ readonly code: string;
183
+ constructor(code: string, message: string, options?: { cause?: unknown }) {
184
+ super(message, options);
185
+ this.code = code;
186
+ }
187
+ }
188
+
189
+ export function isPawabaseError(value: unknown): value is PawabaseError {
190
+ return value instanceof PawabaseError;
191
+ }
192
+
193
+ /** Narrow to an {@link ApiError} with a given status. */
194
+ export function isApiError(value: unknown, status?: number): value is ApiError {
195
+ return value instanceof ApiError && (status === undefined || value.status === status);
196
+ }
197
+
198
+ const STATUS_CODES: Record<number, string> = {
199
+ 400: "bad_request",
200
+ 401: "unauthenticated",
201
+ 403: "forbidden",
202
+ 404: "not_found",
203
+ 405: "method_not_allowed",
204
+ 409: "conflict",
205
+ 413: "payload_too_large",
206
+ 415: "unsupported_media_type",
207
+ 422: "validation_failed",
208
+ 429: "rate_limited",
209
+ };
210
+
211
+ function isRecord(value: unknown): value is Record<string, unknown> {
212
+ return typeof value === "object" && value !== null && !Array.isArray(value);
213
+ }
214
+
215
+ function toIssues(items: unknown[]): ValidationIssue[] {
216
+ return items.map((item) => {
217
+ if (isRecord(item)) {
218
+ const loc = Array.isArray(item["loc"]) ? (item["loc"] as Array<string | number>) : [];
219
+ const field =
220
+ typeof item["field"] === "string"
221
+ ? item["field"]
222
+ : loc.filter((part) => part !== "body" && part !== "query" && part !== "path").join(".");
223
+ const message = String(item["msg"] ?? item["message"] ?? "is invalid");
224
+ return {
225
+ loc,
226
+ field,
227
+ message,
228
+ ...(typeof item["type"] === "string" ? { type: item["type"] } : {}),
229
+ ...("input" in item ? { input: item["input"] } : {}),
230
+ };
231
+ }
232
+ return { loc: [], field: "", message: String(item) };
233
+ });
234
+ }
235
+
236
+ function summarise(issues: ValidationIssue[]): string {
237
+ if (!issues.length) return "The request was refused.";
238
+ return issues.map((i) => (i.field ? `${i.field}: ${i.message}` : i.message)).join("; ");
239
+ }
240
+
241
+ /** Parse `Retry-After`: delta seconds or an HTTP date. Returns seconds, or `null`. */
242
+ export function parseRetryAfter(value: string | null | undefined, now = Date.now()): number | null {
243
+ if (!value) return null;
244
+ const seconds = Number(value);
245
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds;
246
+ const date = Date.parse(value);
247
+ if (Number.isNaN(date)) return null;
248
+ return Math.max(0, (date - now) / 1000);
249
+ }
250
+
251
+ /** Turn a failed response into the right error class. */
252
+ export function errorFromResponse(input: {
253
+ status: number;
254
+ body: unknown;
255
+ headers?: Headers | undefined;
256
+ method?: string | undefined;
257
+ url?: string | undefined;
258
+ }): ApiError {
259
+ const { status, body } = input;
260
+ const requestId = input.headers?.get("x-request-id") ?? null;
261
+ let retryAfter = parseRetryAfter(input.headers?.get("retry-after"));
262
+ let message = "";
263
+ let code = STATUS_CODES[status] ?? (status >= 500 ? "server_error" : "http_error");
264
+ let details: unknown;
265
+ let issues: ValidationIssue[] | null = null;
266
+
267
+ if (typeof body === "string") {
268
+ message = body.trim();
269
+ } else if (Array.isArray(body)) {
270
+ issues = toIssues(body);
271
+ message = summarise(issues);
272
+ details = issues;
273
+ } else if (isRecord(body)) {
274
+ const detail = body["detail"];
275
+ const named = body["error"];
276
+ if (typeof named === "string") code = named;
277
+ else if (typeof body["code"] === "string") code = body["code"] as string;
278
+ if (typeof body["message"] === "string") message = body["message"] as string;
279
+ else if (typeof detail === "string") message = detail;
280
+ if (Array.isArray(detail)) {
281
+ issues = toIssues(detail);
282
+ message ||= summarise(issues);
283
+ details = issues;
284
+ } else if (Array.isArray(body["details"])) {
285
+ details = body["details"];
286
+ const asIssues = toIssues(body["details"] as unknown[]);
287
+ if (asIssues.every((issue) => issue.field)) issues = asIssues;
288
+ message ||= summarise(asIssues);
289
+ } else if ("details" in body) {
290
+ details = body["details"];
291
+ }
292
+ if (retryAfter === null && typeof body["retry_after"] === "number") retryAfter = body["retry_after"];
293
+ }
294
+ if (!message) message = `Request failed with status ${status}.`;
295
+
296
+ const policyMatch = /policy '([^']+)' refused/.exec(message);
297
+ const init: ApiErrorInit = {
298
+ status,
299
+ code,
300
+ message,
301
+ details,
302
+ body,
303
+ requestId,
304
+ method: input.method,
305
+ url: input.url,
306
+ retryAfter,
307
+ policy: policyMatch?.[1] ?? null,
308
+ };
309
+
310
+ if (status === 422 || (status === 400 && issues)) {
311
+ return new ValidationError({ ...init, issues: issues ?? [] });
312
+ }
313
+ if (status === 401) return new AuthenticationError(init);
314
+ if (status === 403) return new PermissionError(init);
315
+ if (status === 404) return new NotFoundError(init);
316
+ if (status === 409) return new ConflictError(init);
317
+ if (status === 413) return new PayloadTooLargeError(init);
318
+ if (status === 429) return new RateLimitError(init);
319
+ if (status >= 500) return new ServerError(init);
320
+ return new ApiError(init);
321
+ }
322
+
323
+ /**
324
+ * A query the client refuses to send because the server would answer it wrongly
325
+ * rather than refuse it: two filters on one field (the server keeps only the last),
326
+ * or an `in` value that contains a comma (the server splits on commas).
327
+ */
328
+ export class InvalidQueryError extends PawabaseError {
329
+ override name = "InvalidQueryError";
330
+ }
package/src/files.ts ADDED
@@ -0,0 +1,208 @@
1
+ import { ConfigError } from "./errors.js";
2
+ import type { HttpClient } from "./http.js";
3
+ import type { FileEntry, FileListing, SignedUrl, StorageBucket, StoredObject } from "./types.js";
4
+ import { encodePath, isSecretKey } from "./util.js";
5
+
6
+ /** Anything an upload accepts. Streams upload without buffering but cannot be retried. */
7
+ export type UploadBody =
8
+ | Blob
9
+ | ArrayBuffer
10
+ | ArrayBufferView
11
+ | string
12
+ | ReadableStream<Uint8Array>
13
+ | URLSearchParams
14
+ | FormData;
15
+
16
+ export interface UploadOptions {
17
+ /** The object's content type. Default: the `Blob`'s own type, else `application/octet-stream`. */
18
+ contentType?: string;
19
+ signal?: AbortSignal | null | undefined;
20
+ timeoutMs?: number;
21
+ }
22
+
23
+ export interface TransferOptions {
24
+ signal?: AbortSignal | null | undefined;
25
+ timeoutMs?: number;
26
+ }
27
+
28
+ function cleanKey(key: string): string {
29
+ const cleaned = key.replace(/^\/+/, "");
30
+ if (!cleaned) throw new ConfigError("A storage key cannot be empty.");
31
+ return cleaned;
32
+ }
33
+
34
+ /** Storage buckets declared in the project. */
35
+ export class StorageClient {
36
+ constructor(private readonly http: HttpClient) {}
37
+
38
+ /** The buckets this environment has. */
39
+ async listBuckets(): Promise<StorageBucket[]> {
40
+ return (await this.http.request<{ data: StorageBucket[] }>({ path: "/storage/v1/buckets" })).data;
41
+ }
42
+
43
+ /** Work with one bucket. */
44
+ from(bucket: string): BucketClient {
45
+ return new BucketClient(this.http, bucket);
46
+ }
47
+ }
48
+
49
+ export class BucketClient {
50
+ constructor(
51
+ private readonly http: HttpClient,
52
+ readonly name: string,
53
+ ) {}
54
+
55
+ private object(key: string): string {
56
+ return `/storage/v1/object/${encodeURIComponent(this.name)}/${encodePath(cleanKey(key))}`;
57
+ }
58
+
59
+ /**
60
+ * Upload (or overwrite) an object. The bucket's policy decides who may, and its
61
+ * `accepts` and `max_bytes` limits apply: a refused type or size is an error, not a truncated file.
62
+ */
63
+ upload(key: string, body: UploadBody, options: UploadOptions = {}): Promise<StoredObject> {
64
+ const declared =
65
+ options.contentType ??
66
+ (typeof Blob !== "undefined" && body instanceof Blob && body.type ? body.type : undefined) ??
67
+ (typeof body === "string" ? "text/plain; charset=utf-8" : "application/octet-stream");
68
+ const multipart = typeof FormData !== "undefined" && body instanceof FormData;
69
+ return this.http.request<StoredObject>({
70
+ method: "PUT",
71
+ path: this.object(key),
72
+ body,
73
+ ...(multipart ? {} : { headers: { "content-type": declared } }),
74
+ signal: options.signal,
75
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
76
+ // Replacing an object with itself is safe to repeat.
77
+ idempotent: true,
78
+ });
79
+ }
80
+
81
+ /** The object's bytes. */
82
+ download(key: string, options: TransferOptions = {}): Promise<Blob> {
83
+ return this.http.request<Blob>({ path: this.object(key), parse: "blob", headers: { accept: "*/*" }, ...this.opts(options) });
84
+ }
85
+
86
+ /** The object as text. */
87
+ downloadText(key: string, options: TransferOptions = {}): Promise<string> {
88
+ return this.http.request<string>({ path: this.object(key), parse: "text", headers: { accept: "*/*" }, ...this.opts(options) });
89
+ }
90
+
91
+ /** The object parsed as JSON. */
92
+ async downloadJson<T = unknown>(key: string, options: TransferOptions = {}): Promise<T> {
93
+ return JSON.parse(await this.downloadText(key, options)) as T;
94
+ }
95
+
96
+ /** The object as a stream, for large files. */
97
+ async downloadStream(key: string, options: TransferOptions = {}): Promise<ReadableStream<Uint8Array>> {
98
+ const stream = await this.http.request<ReadableStream<Uint8Array> | null>({
99
+ path: this.object(key),
100
+ parse: "stream",
101
+ headers: { accept: "*/*" },
102
+ ...this.opts(options),
103
+ });
104
+ if (!stream) throw new ConfigError("The response had no body.");
105
+ return stream;
106
+ }
107
+
108
+ /** The raw response, for its headers (`etag`, `content-type`, `content-length`). */
109
+ downloadResponse(key: string, options: TransferOptions = {}): Promise<Response> {
110
+ return this.http.request<Response>({ path: this.object(key), parse: "response", headers: { accept: "*/*" }, ...this.opts(options) });
111
+ }
112
+
113
+ /** Delete an object. A missing object (or one you may not see) is a `NotFoundError`. */
114
+ async remove(key: string, options: TransferOptions = {}): Promise<void> {
115
+ await this.http.request({ method: "DELETE", path: this.object(key), ...this.opts(options) });
116
+ }
117
+
118
+ /** One page of objects below a prefix. Pass the returned `cursor` back to continue. */
119
+ list(options: { prefix?: string; cursor?: string; limit?: number } & TransferOptions = {}): Promise<FileListing> {
120
+ return this.http.request<FileListing>({
121
+ path: `/storage/v1/list/${encodeURIComponent(this.name)}`,
122
+ query: { prefix: options.prefix, cursor: options.cursor, limit: options.limit },
123
+ ...this.opts(options),
124
+ });
125
+ }
126
+
127
+ /** Every object below a prefix, page by page. */
128
+ async *listAll(options: { prefix?: string; pageSize?: number } & TransferOptions = {}): AsyncGenerator<FileEntry, void, void> {
129
+ let cursor = "";
130
+ do {
131
+ const page = await this.list({
132
+ ...(options.prefix !== undefined ? { prefix: options.prefix } : {}),
133
+ ...(cursor ? { cursor } : {}),
134
+ ...(options.pageSize !== undefined ? { limit: options.pageSize } : {}),
135
+ signal: options.signal,
136
+ });
137
+ yield* page.files;
138
+ cursor = page.cursor;
139
+ } while (cursor);
140
+ }
141
+
142
+ /**
143
+ * A URL for an object in a **public** bucket, usable in `<img src>`. The publishable key
144
+ * travels in the URL (it is meant to be public); a secret key is refused.
145
+ */
146
+ getPublicUrl(key: string): string {
147
+ const apiKey = this.http.config.apiKey;
148
+ if (isSecretKey(apiKey)) {
149
+ throw new ConfigError("Refusing to put a secret key in a URL. Use a publishable key, or createSignedUrl().");
150
+ }
151
+ return `${this.http.url(this.object(key))}?apikey=${encodeURIComponent(apiKey)}`;
152
+ }
153
+
154
+ /** A temporary URL that lets anyone with it read the object, with no key. Needs read access yourself. */
155
+ createSignedUrl(key: string, options: { expiresIn?: number } & TransferOptions = {}): Promise<SignedUrl> {
156
+ return this.sign(key, { method: "GET", expires_in: options.expiresIn ?? 300 }, options);
157
+ }
158
+
159
+ /**
160
+ * A temporary URL that lets anyone with it upload one object, pinned to a content type
161
+ * and size. Hand it to a browser or device; it then calls {@link uploadToSignedUrl}.
162
+ */
163
+ createSignedUploadUrl(
164
+ key: string,
165
+ options: { expiresIn?: number; contentType?: string; maxBytes?: number } & TransferOptions = {},
166
+ ): Promise<SignedUrl> {
167
+ return this.sign(
168
+ key,
169
+ {
170
+ method: "PUT",
171
+ expires_in: options.expiresIn ?? 300,
172
+ ...(options.contentType ? { content_type: options.contentType } : {}),
173
+ ...(options.maxBytes ? { max_bytes: options.maxBytes } : {}),
174
+ },
175
+ options,
176
+ );
177
+ }
178
+
179
+ private sign(key: string, body: Record<string, unknown>, options: TransferOptions): Promise<SignedUrl> {
180
+ return this.http.request<SignedUrl>({
181
+ method: "POST",
182
+ path: `/storage/v1/sign/${encodeURIComponent(this.name)}/${encodePath(cleanKey(key))}`,
183
+ body,
184
+ idempotent: true,
185
+ ...this.opts(options),
186
+ });
187
+ }
188
+
189
+ /** Upload through a signed upload URL. Sends no API key or token: the URL is the credential. */
190
+ async uploadToSignedUrl(url: string, body: UploadBody, options: UploadOptions = {}): Promise<StoredObject> {
191
+ const declared =
192
+ options.contentType ??
193
+ (typeof Blob !== "undefined" && body instanceof Blob && body.type ? body.type : "application/octet-stream");
194
+ return this.http.request<StoredObject>({
195
+ method: "PUT",
196
+ path: "",
197
+ absoluteUrl: url,
198
+ body,
199
+ headers: { "content-type": declared },
200
+ signal: options.signal,
201
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
202
+ });
203
+ }
204
+
205
+ private opts(options: TransferOptions): { signal: AbortSignal | null | undefined; timeoutMs?: number } {
206
+ return { signal: options.signal, ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}) };
207
+ }
208
+ }
@@ -0,0 +1,131 @@
1
+ import type { HttpClient } from "./http.js";
2
+ import type { QueryValue } from "./util.js";
3
+ import { encodePath } from "./util.js";
4
+
5
+ export interface CallOptions {
6
+ headers?: Record<string, string>;
7
+ signal?: AbortSignal | null | undefined;
8
+ timeoutMs?: number;
9
+ /** Allow retrying a `POST` after a network failure. Only when running it twice is harmless. */
10
+ idempotent?: boolean;
11
+ }
12
+
13
+ function pass(options: CallOptions): {
14
+ headers?: Record<string, string>;
15
+ signal?: AbortSignal | null | undefined;
16
+ timeoutMs?: number;
17
+ idempotent?: boolean;
18
+ } {
19
+ return {
20
+ ...(options.headers ? { headers: options.headers } : {}),
21
+ signal: options.signal,
22
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
23
+ ...(options.idempotent !== undefined ? { idempotent: options.idempotent } : {}),
24
+ };
25
+ }
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
+ /**
77
+ * Flows with an HTTP trigger that has no path, invoked at `/flows/v1/<name>`.
78
+ * (Flows bound to a path are reached as routes, see {@link RoutesClient}.)
79
+ */
80
+ export class FlowsClient {
81
+ constructor(private readonly http: HttpClient) {}
82
+
83
+ /**
84
+ * Run a flow and return its response body: whatever its `response.return` block sends,
85
+ * or `{ data: … }` with the flow's result when it has none.
86
+ */
87
+ run<T = unknown>(name: string, input?: unknown, options: CallOptions = {}): Promise<T> {
88
+ return this.http.request<T>({
89
+ method: "POST",
90
+ path: `/flows/v1/${encodePath(name)}`,
91
+ body: input === undefined ? undefined : input,
92
+ ...pass(options),
93
+ });
94
+ }
95
+ }
96
+
97
+ export interface RouteOptions extends CallOptions {
98
+ query?: Record<string, QueryValue | QueryValue[]>;
99
+ body?: unknown;
100
+ }
101
+
102
+ /** Custom routes you defined, served under `/rest/v1/<path>`. */
103
+ export class RoutesClient {
104
+ constructor(private readonly http: HttpClient) {}
105
+
106
+ call<T = unknown>(method: string, path: string, options: RouteOptions = {}): Promise<T> {
107
+ return this.http.request<T>({
108
+ method,
109
+ path: `/rest/v1/${encodePath(path.replace(/^\/+/, ""))}`,
110
+ ...(options.query ? { query: options.query } : {}),
111
+ ...(options.body !== undefined ? { body: options.body } : {}),
112
+ ...pass(options),
113
+ });
114
+ }
115
+
116
+ get<T = unknown>(path: string, options: Omit<RouteOptions, "body"> = {}): Promise<T> {
117
+ return this.call<T>("GET", path, options);
118
+ }
119
+ post<T = unknown>(path: string, body?: unknown, options: Omit<RouteOptions, "body"> = {}): Promise<T> {
120
+ return this.call<T>("POST", path, { ...options, body });
121
+ }
122
+ put<T = unknown>(path: string, body?: unknown, options: Omit<RouteOptions, "body"> = {}): Promise<T> {
123
+ return this.call<T>("PUT", path, { ...options, body });
124
+ }
125
+ patch<T = unknown>(path: string, body?: unknown, options: Omit<RouteOptions, "body"> = {}): Promise<T> {
126
+ return this.call<T>("PATCH", path, { ...options, body });
127
+ }
128
+ delete<T = unknown>(path: string, options: Omit<RouteOptions, "body"> = {}): Promise<T> {
129
+ return this.call<T>("DELETE", path, options);
130
+ }
131
+ }