@tailorkit/app 0.1.0-beta.23 → 0.1.0-beta.24

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.
@@ -25,6 +25,11 @@ function defineServer(functions) {
25
25
  return Object.freeze({ functions });
26
26
  }
27
27
 
28
+ // src/database/definition.ts
29
+ function defineDatabase(definition) {
30
+ return Object.freeze({ relations: definition.relations });
31
+ }
32
+
28
33
  // src/server/builder.ts
29
34
  import { z } from "zod";
30
35
  var FunctionBuilder = class _FunctionBuilder {
@@ -32,25 +37,43 @@ var FunctionBuilder = class _FunctionBuilder {
32
37
  args;
33
38
  result;
34
39
  calls;
35
- constructor(kind, args, result, calls) {
40
+ databaseDefinition;
41
+ constructor(kind, args, result, calls, database) {
36
42
  this.kind = kind;
37
43
  this.args = args;
38
44
  this.result = result;
39
45
  this.calls = calls;
46
+ this.databaseDefinition = database;
40
47
  }
41
48
  input(schema) {
42
- return new _FunctionBuilder(this.kind, schema, this.result, this.calls);
49
+ return new _FunctionBuilder(this.kind, schema, this.result, this.calls, this.databaseDefinition);
43
50
  }
44
51
  output(schema) {
45
- return new _FunctionBuilder(this.kind, this.args, schema, this.calls);
52
+ return new _FunctionBuilder(this.kind, this.args, schema, this.calls, this.databaseDefinition);
46
53
  }
47
54
  functions(functions) {
48
- return new _FunctionBuilder(this.kind, this.args, this.result, functions);
55
+ return new _FunctionBuilder(
56
+ this.kind,
57
+ this.args,
58
+ this.result,
59
+ functions,
60
+ this.databaseDefinition
61
+ );
62
+ }
63
+ database(database) {
64
+ return new _FunctionBuilder(
65
+ this.kind,
66
+ this.args,
67
+ this.result,
68
+ this.calls,
69
+ database
70
+ );
49
71
  }
50
72
  handler(handler) {
51
73
  return Object.freeze({
52
74
  kind: this.kind,
53
75
  args: this.args,
76
+ ...this.databaseDefinition ? { database: this.databaseDefinition } : {},
54
77
  ...this.result ? { result: this.result } : {},
55
78
  ...this.kind === "action" ? { functions: this.calls } : {},
56
79
  handler: (context) => handler({ ...context, input: context.args })
@@ -173,6 +196,7 @@ export {
173
196
  mutation,
174
197
  action,
175
198
  defineServer,
199
+ defineDatabase,
176
200
  tk,
177
201
  withInstanceResolvers,
178
202
  table,
@@ -10,7 +10,6 @@ var logosConfigSchema = z.object({
10
10
  var tailorkitConfigSchema = z.strictObject({
11
11
  appId: z.string().min(1).optional(),
12
12
  build: buildConfigSchema.optional(),
13
- client: z.strictObject({ entry: z.string().min(1).default("src/client.ts") }).optional(),
14
13
  host: z.string().url(),
15
14
  logos: logosConfigSchema.optional(),
16
15
  server: z.strictObject({
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  tailorkitConfigSchema
3
- } from "./chunk-GVB2PAK7.js";
3
+ } from "./chunk-DA2TE2FW.js";
4
4
 
5
5
  // src/config/loader.ts
6
6
  import { cosmiconfig } from "cosmiconfig";
@@ -14,11 +14,6 @@ var loadTailorKitConfig = async (configPath, cwd = process.cwd()) => {
14
14
  if (result === null) {
15
15
  throw new Error(`Could not find tailorkit config from ${searchFrom}.`);
16
16
  }
17
- if (result.config && typeof result.config === "object" && "storage" in result.config) {
18
- throw new Error(
19
- "The legacy storage configuration was removed. Use server configuration and @tailorkit/app."
20
- );
21
- }
22
17
  return {
23
18
  config: tailorkitConfigSchema.parse(result.config),
24
19
  filepath: result.filepath,
@@ -0,0 +1,307 @@
1
+ import {
2
+ AppError,
3
+ appError
4
+ } from "./chunk-IJBQ77AH.js";
5
+
6
+ // src/client/connection.ts
7
+ import { createORPCClient } from "@orpc/client";
8
+ import { RPCLink as FetchLink } from "@orpc/client/fetch";
9
+ import { RPCLink } from "@orpc/client/websocket";
10
+
11
+ // src/client/session.ts
12
+ var APP_SESSION_RENEWAL_MS = 6e4;
13
+ function sessionRenewalDelay(session) {
14
+ return Math.max(500, session.expiresAt - Date.now() - APP_SESSION_RENEWAL_MS);
15
+ }
16
+ function createSessionProvider(options) {
17
+ const base = new URL(options.baseUrl, globalThis.location?.href);
18
+ if (!base.pathname.endsWith("/")) {
19
+ base.pathname += "/";
20
+ }
21
+ let cached;
22
+ let pending;
23
+ return (input) => {
24
+ if (!input.refresh && cached && cached.expiresAt > Date.now() + APP_SESSION_RENEWAL_MS) {
25
+ return Promise.resolve(cached);
26
+ }
27
+ pending ??= (async () => {
28
+ const response = await (options.fetch ?? fetch)(new URL("backend/session", base), {
29
+ method: "POST",
30
+ credentials: "same-origin",
31
+ headers: { "content-type": "application/json" },
32
+ body: JSON.stringify({ appId: options.appId })
33
+ }).catch(() => {
34
+ throw new AppError("UNAVAILABLE", "Unable to reach the app session provider");
35
+ });
36
+ if (!response.ok) {
37
+ const code = response.status === 403 ? "FORBIDDEN" : "UNAVAILABLE";
38
+ throw new AppError(
39
+ response.status === 401 ? "UNAUTHORIZED" : code,
40
+ "Unable to authorize the app backend"
41
+ );
42
+ }
43
+ const value = await response.json();
44
+ if (typeof value.token !== "string" || typeof value.expiresAt !== "number" || typeof value.url !== "string") {
45
+ throw new AppError("UNAVAILABLE", "Invalid app session");
46
+ }
47
+ cached = { token: value.token, expiresAt: value.expiresAt, url: value.url };
48
+ return cached;
49
+ })().finally(() => {
50
+ pending = void 0;
51
+ });
52
+ return pending;
53
+ };
54
+ }
55
+
56
+ // src/client/reference.ts
57
+ function reference(name, _kind) {
58
+ return Object.freeze({ name });
59
+ }
60
+ function createApi() {
61
+ function group(path) {
62
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
63
+ get(_target, name) {
64
+ if (typeof name !== "string" || name === "then") return;
65
+ if (name === "name" && path) return path;
66
+ return group(path ? `${path}.${name}` : name);
67
+ }
68
+ });
69
+ }
70
+ return group("");
71
+ }
72
+
73
+ // src/client/connection.ts
74
+ function createClient(options = {}) {
75
+ let closed = false;
76
+ let socket;
77
+ let timer;
78
+ const stops = /* @__PURE__ */ new Set();
79
+ const requests = /* @__PURE__ */ new Set();
80
+ const getSession = options.getSession ?? ((request) => {
81
+ const bridge = globalThis.__tailorkitBackendSession;
82
+ if (!bridge) {
83
+ throw new AppError("UNAVAILABLE", "The host JWT bridge is unavailable");
84
+ }
85
+ return bridge(request);
86
+ });
87
+ const wire = createORPCClient(
88
+ new RPCLink({
89
+ connect: async (info) => {
90
+ if (closed) {
91
+ throw new AppError("UNAVAILABLE", "App client closed");
92
+ }
93
+ const session = await getSession({ refresh: info.totalAttempt > 1 });
94
+ if (closed) {
95
+ throw new AppError("UNAVAILABLE", "App client closed");
96
+ }
97
+ const url = backendUrl(session, "queries");
98
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
99
+ socket = await (options.connect ?? ((url2, protocols) => new WebSocket(url2, protocols)))(
100
+ url.href,
101
+ ["tailorkit", `jwt.${session.token}`]
102
+ );
103
+ clearTimeout(timer);
104
+ timer = setTimeout(
105
+ () => socket?.close(1e3, "Renew authentication"),
106
+ sessionRenewalDelay(session)
107
+ );
108
+ return socket;
109
+ },
110
+ reconnect: {
111
+ enabled: true,
112
+ delay: (info) => info.attempt === 1 ? 0 : options.retryDelayMs ?? 1e3
113
+ }
114
+ })
115
+ );
116
+ async function call(kind, input) {
117
+ if (closed) {
118
+ throw new AppError("UNAVAILABLE", "App client closed");
119
+ }
120
+ const controller = new AbortController();
121
+ requests.add(controller);
122
+ try {
123
+ const invocation = kind === "mutations" ? structuredClone(input) : input;
124
+ let attempt = 0;
125
+ let refresh = false;
126
+ let refreshed = false;
127
+ while (true) {
128
+ try {
129
+ if (closed) {
130
+ throw new AppError("UNAVAILABLE", "App client closed");
131
+ }
132
+ const session = await getSession({ refresh });
133
+ refresh = false;
134
+ if (closed) {
135
+ throw new AppError("UNAVAILABLE", "App client closed");
136
+ }
137
+ const url = backendUrl(session, kind);
138
+ url.pathname = url.pathname.slice(0, -kind.length).replace(/\/$/u, "");
139
+ const http = createORPCClient(
140
+ new FetchLink({
141
+ url: url.pathname,
142
+ origin: url.origin,
143
+ headers: { authorization: `Bearer ${session.token}` },
144
+ fetch: async (url2, init) => {
145
+ let response;
146
+ try {
147
+ response = await (options.fetch ?? fetch)(url2, { ...init, credentials: "omit" });
148
+ } catch {
149
+ throw new AppError("UNAVAILABLE", "App connection interrupted");
150
+ }
151
+ if ([502, 503, 504].includes(response.status)) {
152
+ throw new AppError("UNAVAILABLE", "App backend temporarily unavailable");
153
+ }
154
+ if (kind === "mutations" && response.body) {
155
+ try {
156
+ return new Response(await response.arrayBuffer(), {
157
+ status: response.status,
158
+ headers: response.headers
159
+ });
160
+ } catch {
161
+ throw new AppError("UNAVAILABLE", "App connection interrupted");
162
+ }
163
+ }
164
+ return response;
165
+ }
166
+ })
167
+ );
168
+ if (kind === "mutations") {
169
+ return await http.mutations(invocation, {
170
+ signal: controller.signal
171
+ });
172
+ }
173
+ return await http[kind](invocation, { signal: controller.signal });
174
+ } catch (error) {
175
+ const failure = appError(error);
176
+ if (kind !== "mutations" || closed || controller.signal.aborted) {
177
+ throw failure;
178
+ }
179
+ if (failure.code === "UNAUTHORIZED" && !refreshed) {
180
+ refresh = true;
181
+ refreshed = true;
182
+ continue;
183
+ }
184
+ if (failure.code !== "UNAVAILABLE") {
185
+ throw failure;
186
+ }
187
+ await waitForRetry(
188
+ controller.signal,
189
+ Math.min((options.retryDelayMs ?? 1e3) * 2 ** Math.min(attempt++, 5), 3e4)
190
+ );
191
+ }
192
+ }
193
+ } catch (error) {
194
+ throw appError(error);
195
+ } finally {
196
+ requests.delete(controller);
197
+ }
198
+ }
199
+ return {
200
+ query: (ref, ...[args]) => call("queries", { name: ref.name, args }),
201
+ mutate: (ref, ...[args, settings]) => call("mutations", {
202
+ name: ref.name,
203
+ args,
204
+ requestId: settings?.requestId ?? crypto.randomUUID()
205
+ }),
206
+ action: (ref, ...[args]) => call("actions", { name: ref.name, args }),
207
+ subscribe(ref, args, next, settings = {}) {
208
+ if (closed) {
209
+ throw new AppError("UNAVAILABLE", "App client closed");
210
+ }
211
+ const controller = new AbortController();
212
+ const stop = () => {
213
+ controller.abort();
214
+ stops.delete(stop);
215
+ };
216
+ stops.add(stop);
217
+ const run = async () => {
218
+ while (!controller.signal.aborted) {
219
+ try {
220
+ const stream = await wire.subscribe(
221
+ { name: ref.name, args },
222
+ { signal: controller.signal }
223
+ );
224
+ for await (const value of stream) {
225
+ if (controller.signal.aborted || closed) {
226
+ break;
227
+ }
228
+ next(value);
229
+ }
230
+ } catch (error) {
231
+ if (controller.signal.aborted || closed) {
232
+ break;
233
+ }
234
+ const failure = appError(error);
235
+ if (!(error instanceof Error && error.name === "AbortError")) {
236
+ settings.onError?.(failure);
237
+ }
238
+ if (!["UNAUTHORIZED", "UNAVAILABLE", "INTERNAL_SERVER_ERROR"].includes(failure.code)) {
239
+ break;
240
+ }
241
+ }
242
+ if (!controller.signal.aborted && !closed) {
243
+ await waitForRetry(controller.signal, options.retryDelayMs ?? 1e3);
244
+ }
245
+ }
246
+ stop();
247
+ };
248
+ void run().catch((error) => {
249
+ stop();
250
+ settings.onError?.(appError(error));
251
+ });
252
+ return stop;
253
+ },
254
+ close() {
255
+ closed = true;
256
+ for (const stop of stops) {
257
+ stop();
258
+ }
259
+ for (const request of requests) {
260
+ request.abort();
261
+ }
262
+ clearTimeout(timer);
263
+ socket?.close();
264
+ }
265
+ };
266
+ }
267
+ function backendUrl(session, route) {
268
+ const url = new URL(session.url);
269
+ if (url.username || url.password || url.search || url.hash) {
270
+ throw new AppError("BAD_REQUEST", "Invalid app backend URL");
271
+ }
272
+ if (url.protocol === "wss:") {
273
+ url.protocol = "https:";
274
+ }
275
+ if (url.protocol === "ws:") {
276
+ url.protocol = "http:";
277
+ }
278
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
279
+ throw new AppError("BAD_REQUEST", "App backends require HTTPS");
280
+ }
281
+ if (session.expiresAt <= Date.now() + 1e3) {
282
+ throw new AppError("UNAUTHORIZED", "App token expired");
283
+ }
284
+ url.pathname = `${url.pathname.replace(/\/$/u, "")}/${route}`;
285
+ return url;
286
+ }
287
+ function waitForRetry(signal, delayMs) {
288
+ if (signal.aborted) {
289
+ return Promise.resolve();
290
+ }
291
+ return new Promise((resolve) => {
292
+ const wake = () => {
293
+ clearTimeout(timer);
294
+ signal.removeEventListener("abort", wake);
295
+ resolve();
296
+ };
297
+ const timer = setTimeout(wake, delayMs);
298
+ signal.addEventListener("abort", wake, { once: true });
299
+ });
300
+ }
301
+
302
+ export {
303
+ createSessionProvider,
304
+ reference,
305
+ createApi,
306
+ createClient
307
+ };