@alchemy.run/turso 2.0.0-beta.84 → 2.0.0-beta.86

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 (62) hide show
  1. package/lib/ApiToken.d.ts +9 -0
  2. package/lib/ApiToken.d.ts.map +1 -1
  3. package/lib/ApiToken.js +10 -2
  4. package/lib/ApiToken.js.map +1 -1
  5. package/lib/AuthProvider.d.ts +1 -1
  6. package/lib/AuthProvider.d.ts.map +1 -1
  7. package/lib/AuthProvider.js +1 -1
  8. package/lib/AuthProvider.js.map +1 -1
  9. package/lib/Connect.d.ts.map +1 -1
  10. package/lib/Connect.js.map +1 -1
  11. package/lib/Contributions.d.ts +1 -1
  12. package/lib/Contributions.d.ts.map +1 -1
  13. package/lib/Contributions.js.map +1 -1
  14. package/lib/Credentials.d.ts +1 -1
  15. package/lib/Credentials.d.ts.map +1 -1
  16. package/lib/Credentials.js +11 -15
  17. package/lib/Credentials.js.map +1 -1
  18. package/lib/Database.d.ts +36 -1
  19. package/lib/Database.d.ts.map +1 -1
  20. package/lib/Database.js +49 -20
  21. package/lib/Database.js.map +1 -1
  22. package/lib/DatabaseToken.d.ts +9 -0
  23. package/lib/DatabaseToken.d.ts.map +1 -1
  24. package/lib/DatabaseToken.js +6 -1
  25. package/lib/DatabaseToken.js.map +1 -1
  26. package/lib/Group.d.ts +29 -1
  27. package/lib/Group.d.ts.map +1 -1
  28. package/lib/Group.js +30 -5
  29. package/lib/Group.js.map +1 -1
  30. package/lib/GroupToken.d.ts +9 -0
  31. package/lib/GroupToken.d.ts.map +1 -1
  32. package/lib/GroupToken.js +6 -1
  33. package/lib/GroupToken.js.map +1 -1
  34. package/lib/Hrana.d.ts.map +1 -1
  35. package/lib/Hrana.js +5 -1
  36. package/lib/Hrana.js.map +1 -1
  37. package/lib/ManageDatabases.d.ts.map +1 -1
  38. package/lib/ManageDatabases.js +1 -1
  39. package/lib/ManageDatabases.js.map +1 -1
  40. package/lib/OrganizationInvite.d.ts.map +1 -1
  41. package/lib/OrganizationInvite.js.map +1 -1
  42. package/lib/OrganizationMember.d.ts.map +1 -1
  43. package/lib/OrganizationMember.js +1 -1
  44. package/lib/OrganizationMember.js.map +1 -1
  45. package/lib/OrganizationSettings.d.ts.map +1 -1
  46. package/lib/OrganizationSettings.js.map +1 -1
  47. package/lib/Providers.d.ts +1 -1
  48. package/lib/Providers.d.ts.map +1 -1
  49. package/lib/Providers.js.map +1 -1
  50. package/lib/index.d.ts.map +1 -1
  51. package/lib/index.js.map +1 -1
  52. package/package.json +9 -9
  53. package/src/ApiToken.ts +12 -2
  54. package/src/AuthProvider.ts +1 -1
  55. package/src/Credentials.ts +22 -49
  56. package/src/Database.ts +79 -24
  57. package/src/DatabaseToken.ts +11 -1
  58. package/src/Group.ts +52 -6
  59. package/src/GroupToken.ts +9 -1
  60. package/src/Hrana.ts +3 -1
  61. package/src/ManageDatabases.ts +1 -1
  62. package/src/OrganizationMember.ts +1 -1
@@ -1,25 +1,13 @@
1
1
  import { ConfigError } from "@distilled.cloud/core/errors";
2
- import {
3
- Credentials,
4
- DEFAULT_API_BASE_URL,
5
- type Config as TursoClientConfig,
6
- } from "@distilled.cloud/turso/Credentials";
2
+ import { Credentials, DEFAULT_API_BASE_URL } from "@distilled.cloud/turso/Credentials";
7
3
  import * as turso from "@distilled.cloud/turso/turso";
8
- import {
9
- deferUntilFirstUse,
10
- orDieCredentialsUnavailable,
11
- resolveProviderConfig,
12
- } from "alchemy/Auth/Resolve";
4
+ import { lazyCredentials, orDieCredentialsUnavailable } from "alchemy/Auth/Resolve";
13
5
  import * as Context from "effect/Context";
14
6
  import * as Effect from "effect/Effect";
15
7
  import * as HttpClient from "effect/http/HttpClient";
16
8
  import * as Layer from "effect/Layer";
17
9
  import * as Redacted from "effect/Redacted";
18
- import {
19
- TURSO_AUTH_PROVIDER_NAME,
20
- type TursoAuthConfig,
21
- type TursoResolvedCredentials,
22
- } from "./AuthProvider.ts";
10
+ import { TURSO_AUTH_PROVIDER_NAME, type TursoResolvedCredentials } from "./AuthProvider.ts";
23
11
 
24
12
  export {
25
13
  Credentials,
@@ -61,17 +49,19 @@ const discoverOrganization = Effect.gen(function* () {
61
49
  });
62
50
 
63
51
  const make = (
64
- resolve: Effect.Effect<TursoResolvedCredentials>,
52
+ resolve: Effect.Effect<TursoResolvedCredentials, ConfigError>,
65
53
  ): Effect.Effect<Context.Context<Credentials | Organization>, never, HttpClient.HttpClient> =>
66
54
  Effect.gen(function* () {
67
55
  const http = yield* HttpClient.HttpClient;
68
- const cached = yield* Effect.cached(resolve);
69
- const credentials: Effect.Effect<TursoClientConfig> = Effect.map(cached, (creds) => ({
56
+ const credentials = Effect.map(resolve, (creds) => ({
70
57
  apiKey: creds.apiToken,
71
58
  apiBaseUrl: creds.apiBaseUrl,
72
59
  }));
73
- const organization = yield* Effect.cached(
74
- Effect.flatMap(cached, (creds) =>
60
+ // `Organization` is typed as never failing, so a credentials failure dies
61
+ // here; the engine still reports it as `CredentialsRequired`.
62
+ const organization = yield* resolve.pipe(
63
+ Effect.orDie,
64
+ Effect.flatMap((creds) =>
75
65
  creds.organization !== undefined && creds.organization.length > 0
76
66
  ? Effect.succeed(creds.organization)
77
67
  : discoverOrganization.pipe(
@@ -80,6 +70,7 @@ const make = (
80
70
  orDieCredentialsUnavailable(TURSO_AUTH_PROVIDER_NAME),
81
71
  ),
82
72
  ),
73
+ Effect.cached,
83
74
  );
84
75
  return Context.make(Credentials, credentials).pipe(Context.add(Organization, organization));
85
76
  });
@@ -98,18 +89,13 @@ export const fromToken = (input: {
98
89
  organization?: string;
99
90
  apiBaseUrl?: string;
100
91
  }) =>
101
- Layer.effectContext(
102
- make(
103
- Effect.succeed({
104
- type: "apiToken",
105
- apiToken:
106
- typeof input.apiToken === "string" ? Redacted.make(input.apiToken) : input.apiToken,
107
- apiBaseUrl: input.apiBaseUrl ?? DEFAULT_API_BASE_URL,
108
- organization: input.organization,
109
- source: { type: "stored" },
110
- }),
111
- ),
112
- );
92
+ Effect.succeed<TursoResolvedCredentials>({
93
+ type: "apiToken",
94
+ apiToken: typeof input.apiToken === "string" ? Redacted.make(input.apiToken) : input.apiToken,
95
+ apiBaseUrl: input.apiBaseUrl ?? DEFAULT_API_BASE_URL,
96
+ organization: input.organization,
97
+ source: { type: "stored" },
98
+ }).pipe(make, Layer.effectContext);
113
99
 
114
100
  /**
115
101
  * Build Turso `Credentials` + `Organization` that resolve through the
@@ -119,21 +105,8 @@ export const fromToken = (input: {
119
105
  */
120
106
  export const fromAuthProvider = () =>
121
107
  Layer.effectContext(
122
- Effect.gen(function* () {
123
- const resolve = yield* resolveProviderConfig<TursoAuthConfig, TursoResolvedCredentials>(
124
- TURSO_AUTH_PROVIDER_NAME,
125
- ).pipe(
126
- Effect.flatMap(({ profileName, resolve }) =>
127
- Effect.mapError(
128
- resolve,
129
- (e) =>
130
- new ConfigError({
131
- message: `Failed to resolve Turso credentials from ${profileName === undefined ? "the environment" : `profile '${profileName}'`}: ${e.message}`,
132
- }),
133
- ),
134
- ),
135
- deferUntilFirstUse,
136
- );
137
- return yield* make(resolve.pipe(orDieCredentialsUnavailable(TURSO_AUTH_PROVIDER_NAME)));
138
- }),
108
+ lazyCredentials<TursoResolvedCredentials, ConfigError>()(
109
+ TURSO_AUTH_PROVIDER_NAME,
110
+ (creds) => creds,
111
+ ).pipe(Effect.flatMap(make)),
139
112
  );
package/src/Database.ts CHANGED
@@ -6,13 +6,14 @@ import {
6
6
  stampedOf,
7
7
  type MigrationsInput,
8
8
  } from "@alchemy.run/sql/Migrations";
9
- import { hashImports, readSqlFile, splitSqlStatements } from "@alchemy.run/sql/SqlFile";
9
+ import { applySqlImports, hashImports, splitSqlStatements } from "@alchemy.run/sql/SqlFile";
10
10
  import * as turso from "@distilled.cloud/turso/turso";
11
11
  import { Unowned } from "alchemy/AdoptPolicy";
12
12
  import { deepEqual, isResolved } from "alchemy/Diff";
13
13
  import { createPhysicalName } from "alchemy/PhysicalName";
14
14
  import * as Provider from "alchemy/Provider";
15
15
  import { Resource } from "alchemy/Resource";
16
+ import * as Data from "effect/Data";
16
17
  import * as Effect from "effect/Effect";
17
18
  import * as HttpClient from "effect/http/HttpClient";
18
19
  import * as Redacted from "effect/Redacted";
@@ -20,6 +21,11 @@ import { organization } from "./Credentials.ts";
20
21
  import * as Hrana from "./Hrana.ts";
21
22
  import type { Providers } from "./Providers.ts";
22
23
 
24
+ export class DatabaseUnexpectedResponse extends Data.TaggedError("DatabaseUnexpectedResponse")<{
25
+ readonly message: string;
26
+ readonly cause?: unknown;
27
+ }> {}
28
+
23
29
  export interface DatabaseSeed {
24
30
  /**
25
31
  * `"database"` copies an existing database (optionally at a point in
@@ -128,6 +134,13 @@ export interface DatabaseProps {
128
134
  * Bookkeeping lives in Alchemy's `__alchemy_migrations` table. A database
129
135
  * previously migrated by drizzle-kit is adopted by a one-way conversion on
130
136
  * first deploy.
137
+ *
138
+ * History is forward-only and linear. A new file must sort after every
139
+ * applied one, or the deploy fails with `NonLinearMigrationHistoryError`;
140
+ * an edited or deleted applied file fails with
141
+ * `RewrittenMigrationHistoryError`. Renaming an applied file without
142
+ * changing its contents keeps it applied. See
143
+ * [Migrations](https://alchemy.run/sql/effect-sql/migrations#change-migration-history-safely).
131
144
  */
132
145
  migrations?: MigrationsInput;
133
146
  /**
@@ -185,9 +198,19 @@ export type Database = Resource<
185
198
  DatabaseProps,
186
199
  DatabaseAttributes,
187
200
  never,
188
- Providers
201
+ Providers,
202
+ DatabaseLookup
189
203
  >;
190
204
 
205
+ /**
206
+ * Props for {@link Database.lookup}: find a database this stack
207
+ * doesn't manage by its name, in the credentialed organization.
208
+ */
209
+ export interface DatabaseLookup {
210
+ /** Database name. */
211
+ name: string;
212
+ }
213
+
191
214
  /**
192
215
  * A Turso database: a SQLite database that lives in a {@link Group}.
193
216
  * Connect to it from a Worker or Lambda with {@link Connect}, or create a
@@ -246,6 +269,17 @@ export type Database = Resource<
246
269
  * const db = yield* Drizzle.LibSQL(conn);
247
270
  * ```
248
271
  *
272
+ * ### Looking up an existing database
273
+ * `Database.lookup` references a database Alchemy doesn't manage (created with
274
+ * the Turso CLI or by another stack). It is read during plan/deploy, never
275
+ * created, updated or deleted.
276
+ *
277
+ * **Example:** Mint a token for an existing database
278
+ * ```typescript
279
+ * const db = Turso.Database.lookup({ name: "app" });
280
+ * const token = yield* Turso.DatabaseToken("AppToken", { database: db.name });
281
+ * ```
282
+ *
249
283
  * @resource
250
284
  * @product Database
251
285
  */
@@ -276,7 +310,9 @@ const withConnection = <A, E, R>(
276
310
  authorization: "full-access",
277
311
  });
278
312
  if (!jwt) {
279
- return yield* Effect.fail(new Error(`Turso returned no token for ${database.name}`));
313
+ return yield* new DatabaseUnexpectedResponse({
314
+ message: `Turso returned no token for ${database.name}`,
315
+ });
280
316
  }
281
317
  return yield* use({ url: `https://${database.hostname}`, authToken: jwt }, client);
282
318
  });
@@ -288,7 +324,7 @@ export const DatabaseProvider = () =>
288
324
  const observe = (org: string, name: string) =>
289
325
  turso.getDatabase({ organizationSlug: org, databaseName: name }).pipe(
290
326
  Effect.map((res) => res.database),
291
- Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
327
+ Effect.catchTag("NotFound", () => Effect.undefined),
292
328
  );
293
329
 
294
330
  const toAttrs = (
@@ -351,7 +387,19 @@ export const DatabaseProvider = () =>
351
387
  }
352
388
  }
353
389
  }),
354
- read: Effect.fn(function* ({ id, olds, output }) {
390
+ lookup: Effect.fn(function* ({ name }) {
391
+ const org = yield* organization;
392
+ const db = yield* observe(org, name);
393
+ if (!db) return undefined;
394
+ return toAttrs(org, db, yield* getConfig(org, name), {
395
+ migrationsDir: undefined,
396
+ migrationsTable: undefined,
397
+ migrationsHashes: {},
398
+ importHashes: {},
399
+ });
400
+ }),
401
+
402
+ read: Effect.fn(function* ({ id, olds, output, previous }) {
355
403
  const org = output?.organization ?? (yield* organization);
356
404
  const name = output?.name ?? (yield* databaseName(id, olds?.name));
357
405
  const db = yield* observe(org, name);
@@ -363,8 +411,13 @@ export const DatabaseProvider = () =>
363
411
  importHashes: output?.importHashes ?? {},
364
412
  });
365
413
  // Turso has no tags: a database found under an explicit,
366
- // user-chosen name without prior state may belong to someone else.
367
- return output === undefined && olds?.name !== undefined ? Unowned(attrs) : attrs;
414
+ // user-chosen name without prior state may belong to someone else — unless it
415
+ // is the generation a delete-first replacement is about to free.
416
+ const ours =
417
+ output !== undefined ||
418
+ olds?.name === undefined ||
419
+ (previous?.organization === org && previous.name === name);
420
+ return ours ? attrs : Unowned(attrs);
368
421
  }),
369
422
  reconcile: Effect.fn(function* ({ id, news, output, session }) {
370
423
  const org = output?.organization ?? (yield* organization);
@@ -393,7 +446,9 @@ export const DatabaseProvider = () =>
393
446
  .pipe(Effect.catchTag("Conflict", () => Effect.void));
394
447
  db = yield* observe(org, name);
395
448
  if (!db) {
396
- return yield* Effect.fail(new Error(`Turso database ${name} vanished after create`));
449
+ return yield* new DatabaseUnexpectedResponse({
450
+ message: `Turso database ${name} vanished after create`,
451
+ });
397
452
  }
398
453
  }
399
454
 
@@ -447,23 +502,23 @@ export const DatabaseProvider = () =>
447
502
  })
448
503
  : undefined;
449
504
 
450
- const previousImports = output?.importHashes ?? {};
451
- const importHashes: Record<string, string> = {};
452
- if (news.importFiles?.length) {
453
- const root = yield* rootDir;
454
- for (const filePath of news.importFiles) {
455
- const file = yield* readSqlFile(root, filePath);
456
- if (previousImports[filePath] !== file.hash) {
457
- yield* session.note(`Importing ${filePath}...`);
458
- yield* withConnection(org, target, (connection, client) =>
459
- Hrana.transaction(connection, splitSqlStatements(file.sql)).pipe(
460
- Effect.provideService(HttpClient.HttpClient, client),
505
+ const importHashes = yield* applySqlImports({
506
+ importFiles: news.importFiles,
507
+ rootDir: yield* rootDir,
508
+ previous: output?.importHashes,
509
+ apply: (file) =>
510
+ session
511
+ .note(`Importing ${file.id}...`)
512
+ .pipe(
513
+ Effect.andThen(
514
+ withConnection(org, target, (connection, client) =>
515
+ Hrana.transaction(connection, splitSqlStatements(file.sql)).pipe(
516
+ Effect.provideService(HttpClient.HttpClient, client),
517
+ ),
518
+ ),
461
519
  ),
462
- );
463
- }
464
- importHashes[filePath] = file.hash;
465
- }
466
- }
520
+ ),
521
+ });
467
522
 
468
523
  return toAttrs(org, db, config, {
469
524
  ...migrationsAttrs({ input: migrationsInput, run: migrations, output }),
@@ -3,11 +3,19 @@ import { deepEqual } from "alchemy/Diff";
3
3
  import * as Provider from "alchemy/Provider";
4
4
  import { Resource } from "alchemy/Resource";
5
5
  import * as Clock from "effect/Clock";
6
+ import * as Data from "effect/Data";
6
7
  import * as Effect from "effect/Effect";
7
8
  import * as Redacted from "effect/Redacted";
8
9
  import { organization } from "./Credentials.ts";
9
10
  import type { Providers } from "./Providers.ts";
10
11
 
12
+ export class DatabaseTokenUnexpectedResponse extends Data.TaggedError(
13
+ "DatabaseTokenUnexpectedResponse",
14
+ )<{
15
+ readonly message: string;
16
+ readonly cause?: unknown;
17
+ }> {}
18
+
11
19
  /** Access level of a Turso SQL auth token. */
12
20
  export type TokenAuthorization = "full-access" | "read-only";
13
21
 
@@ -185,7 +193,9 @@ export const DatabaseTokenProvider = () =>
185
193
  : undefined,
186
194
  });
187
195
  if (!jwt) {
188
- return yield* Effect.fail(new Error(`Turso returned no token for ${news.database}`));
196
+ return yield* new DatabaseTokenUnexpectedResponse({
197
+ message: `Turso returned no token for ${news.database}`,
198
+ });
189
199
  }
190
200
  return {
191
201
  token: jwt,
package/src/Group.ts CHANGED
@@ -4,10 +4,16 @@ import { deepEqual, isResolved } from "alchemy/Diff";
4
4
  import { createPhysicalName } from "alchemy/PhysicalName";
5
5
  import * as Provider from "alchemy/Provider";
6
6
  import { Resource } from "alchemy/Resource";
7
+ import * as Data from "effect/Data";
7
8
  import * as Effect from "effect/Effect";
8
9
  import { organization } from "./Credentials.ts";
9
10
  import type { Providers } from "./Providers.ts";
10
11
 
12
+ export class GroupUnexpectedResponse extends Data.TaggedError("GroupUnexpectedResponse")<{
13
+ readonly message: string;
14
+ readonly cause?: unknown;
15
+ }> {}
16
+
11
17
  /**
12
18
  * A Turso location code, e.g. `"aws-us-east-1"`. List the locations
13
19
  * available to your organization with `turso db locations`.
@@ -75,7 +81,23 @@ export interface GroupAttributes {
75
81
  deleteProtection: boolean;
76
82
  }
77
83
 
78
- export type Group = Resource<"Turso.Group", GroupProps, GroupAttributes, never, Providers>;
84
+ export type Group = Resource<
85
+ "Turso.Group",
86
+ GroupProps,
87
+ GroupAttributes,
88
+ never,
89
+ Providers,
90
+ GroupLookup
91
+ >;
92
+
93
+ /**
94
+ * Props for {@link Group.lookup}: find a group this stack
95
+ * doesn't manage by its name, in the credentialed organization.
96
+ */
97
+ export interface GroupLookup {
98
+ /** Group name. */
99
+ name: string;
100
+ }
79
101
 
80
102
  /**
81
103
  * A Turso group. Every Turso database belongs to one group, and the group
@@ -106,6 +128,17 @@ export type Group = Resource<"Turso.Group", GroupProps, GroupAttributes, never,
106
128
  * const db = yield* Turso.Database("Db", { group: group.name });
107
129
  * ```
108
130
  *
131
+ * ### Looking up an existing group
132
+ * `Group.lookup` references a group Alchemy doesn't manage (created with
133
+ * the Turso CLI or by another stack). It is read during plan/deploy, never
134
+ * created, updated or deleted.
135
+ *
136
+ * **Example:** Create a database in an existing group
137
+ * ```typescript
138
+ * const group = Turso.Group.lookup({ name: "default" });
139
+ * const db = yield* Turso.Database("App", { group: group.name });
140
+ * ```
141
+ *
109
142
  * @resource
110
143
  * @product Group
111
144
  */
@@ -125,7 +158,7 @@ export const GroupProvider = () =>
125
158
  const observe = (org: string, name: string) =>
126
159
  turso.getGroup({ organizationSlug: org, groupName: name }).pipe(
127
160
  Effect.map((res) => res.group),
128
- Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
161
+ Effect.catchTag("NotFound", () => Effect.undefined),
129
162
  );
130
163
 
131
164
  const toAttrs = (
@@ -157,15 +190,26 @@ export const GroupProvider = () =>
157
190
  return { action: "replace", deleteFirst: news.name !== undefined } as const;
158
191
  }
159
192
  }),
160
- read: Effect.fn(function* ({ id, olds, output }) {
193
+ lookup: Effect.fn(function* ({ name }) {
194
+ const org = yield* organization;
195
+ const group = yield* observe(org, name);
196
+ return group ? toAttrs(org, group, group.delete_protection ?? false) : undefined;
197
+ }),
198
+
199
+ read: Effect.fn(function* ({ id, olds, output, previous }) {
161
200
  const org = output?.organization ?? (yield* organization);
162
201
  const name = output?.name ?? (yield* groupName(id, olds?.name));
163
202
  const group = yield* observe(org, name);
164
203
  if (!group) return undefined;
165
204
  const attrs = toAttrs(org, group, group.delete_protection ?? false);
166
205
  // Turso has no tags: a group found under an explicit, user-chosen
167
- // name without prior state may belong to someone else.
168
- return output === undefined && olds?.name !== undefined ? Unowned(attrs) : attrs;
206
+ // name without prior state may belong to someone else — unless it
207
+ // is the generation a delete-first replacement is about to free.
208
+ const ours =
209
+ output !== undefined ||
210
+ olds?.name === undefined ||
211
+ (previous?.organization === org && previous.name === name);
212
+ return ours ? attrs : Unowned(attrs);
169
213
  }),
170
214
  reconcile: Effect.fn(function* ({ id, news, output, session }) {
171
215
  const org = output?.organization ?? (yield* organization);
@@ -190,7 +234,9 @@ export const GroupProvider = () =>
190
234
  Effect.catchTag("Conflict", () => observe(org, name)),
191
235
  );
192
236
  if (!group) {
193
- return yield* Effect.fail(new Error(`Turso group ${name} vanished after create`));
237
+ return yield* new GroupUnexpectedResponse({
238
+ message: `Turso group ${name} vanished after create`,
239
+ });
194
240
  }
195
241
  }
196
242
 
package/src/GroupToken.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as turso from "@distilled.cloud/turso/turso";
2
2
  import * as Provider from "alchemy/Provider";
3
3
  import { Resource } from "alchemy/Resource";
4
+ import * as Data from "effect/Data";
4
5
  import * as Effect from "effect/Effect";
5
6
  import { organization } from "./Credentials.ts";
6
7
  import {
@@ -12,6 +13,11 @@ import {
12
13
  } from "./DatabaseToken.ts";
13
14
  import type { Providers } from "./Providers.ts";
14
15
 
16
+ export class GroupTokenUnexpectedResponse extends Data.TaggedError("GroupTokenUnexpectedResponse")<{
17
+ readonly message: string;
18
+ readonly cause?: unknown;
19
+ }> {}
20
+
15
21
  export interface GroupTokenProps extends SqlTokenProps {
16
22
  /** Name of the group whose databases the token grants access to. */
17
23
  group: string;
@@ -101,7 +107,9 @@ export const GroupTokenProvider = () =>
101
107
  : undefined,
102
108
  });
103
109
  if (!jwt) {
104
- return yield* Effect.fail(new Error(`Turso returned no token for group ${news.group}`));
110
+ return yield* new GroupTokenUnexpectedResponse({
111
+ message: `Turso returned no token for group ${news.group}`,
112
+ });
105
113
  }
106
114
  return {
107
115
  token: jwt,
package/src/Hrana.ts CHANGED
@@ -61,7 +61,9 @@ const toHranaValue = (value: unknown): HranaValue => {
61
61
  if (value instanceof Uint8Array) {
62
62
  return { type: "blob", base64: Buffer.from(value).toString("base64") };
63
63
  }
64
- return { type: "text", value: String(value) };
64
+ if (typeof value === "string") return { type: "text", value };
65
+ if (value instanceof Date) return { type: "text", value: value.toISOString() };
66
+ return { type: "text", value: JSON.stringify(value) };
65
67
  };
66
68
 
67
69
  const fromHranaValue = (value: HranaValue): unknown => {
@@ -159,7 +159,7 @@ export const ManageDatabasesHttp = Layer.effect(
159
159
  return database && database.group === (yield* groupName)
160
160
  ? toManaged(database)
161
161
  : undefined;
162
- }).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))),
162
+ }).pipe(Effect.catchTag("NotFound", () => Effect.undefined)),
163
163
  ),
164
164
  list: () =>
165
165
  authorize(
@@ -64,7 +64,7 @@ export const OrganizationMemberProvider = () =>
64
64
  const observe = (org: string, username: string) =>
65
65
  turso.getOrganizationMember({ organizationSlug: org, username }).pipe(
66
66
  Effect.map(({ member }) => member),
67
- Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
67
+ Effect.catchTag("NotFound", () => Effect.undefined),
68
68
  );
69
69
 
70
70
  return {