@alchemy.run/turso 0.0.0 → 2.0.0-beta.85

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 (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +68 -4
  3. package/lib/ApiToken.d.ts +82 -0
  4. package/lib/ApiToken.d.ts.map +1 -0
  5. package/lib/ApiToken.js +128 -0
  6. package/lib/ApiToken.js.map +1 -0
  7. package/lib/AuthProvider.d.ts +36 -0
  8. package/lib/AuthProvider.d.ts.map +1 -0
  9. package/lib/AuthProvider.js +85 -0
  10. package/lib/AuthProvider.js.map +1 -0
  11. package/lib/Connect.d.ts +79 -0
  12. package/lib/Connect.d.ts.map +1 -0
  13. package/lib/Connect.js +22 -0
  14. package/lib/Connect.js.map +1 -0
  15. package/lib/Contributions.d.ts +7 -0
  16. package/lib/Contributions.d.ts.map +1 -0
  17. package/lib/Contributions.js +6 -0
  18. package/lib/Contributions.js.map +1 -0
  19. package/lib/Credentials.d.ts +39 -0
  20. package/lib/Credentials.d.ts.map +1 -0
  21. package/lib/Credentials.js +76 -0
  22. package/lib/Credentials.js.map +1 -0
  23. package/lib/Database.d.ts +226 -0
  24. package/lib/Database.d.ts.map +1 -0
  25. package/lib/Database.js +277 -0
  26. package/lib/Database.js.map +1 -0
  27. package/lib/DatabaseToken.d.ts +104 -0
  28. package/lib/DatabaseToken.d.ts.map +1 -0
  29. package/lib/DatabaseToken.js +133 -0
  30. package/lib/DatabaseToken.js.map +1 -0
  31. package/lib/Group.d.ts +102 -0
  32. package/lib/Group.d.ts.map +1 -0
  33. package/lib/Group.js +171 -0
  34. package/lib/Group.js.map +1 -0
  35. package/lib/GroupToken.d.ts +57 -0
  36. package/lib/GroupToken.d.ts.map +1 -0
  37. package/lib/GroupToken.js +88 -0
  38. package/lib/GroupToken.js.map +1 -0
  39. package/lib/Hrana.d.ts +42 -0
  40. package/lib/Hrana.d.ts.map +1 -0
  41. package/lib/Hrana.js +109 -0
  42. package/lib/Hrana.js.map +1 -0
  43. package/lib/ManageDatabases.d.ts +73 -0
  44. package/lib/ManageDatabases.d.ts.map +1 -0
  45. package/lib/ManageDatabases.js +81 -0
  46. package/lib/ManageDatabases.js.map +1 -0
  47. package/lib/OrganizationInvite.d.ts +46 -0
  48. package/lib/OrganizationInvite.d.ts.map +1 -0
  49. package/lib/OrganizationInvite.js +72 -0
  50. package/lib/OrganizationInvite.js.map +1 -0
  51. package/lib/OrganizationMember.d.ts +47 -0
  52. package/lib/OrganizationMember.d.ts.map +1 -0
  53. package/lib/OrganizationMember.js +81 -0
  54. package/lib/OrganizationMember.js.map +1 -0
  55. package/lib/OrganizationSettings.d.ts +52 -0
  56. package/lib/OrganizationSettings.d.ts.map +1 -0
  57. package/lib/OrganizationSettings.js +93 -0
  58. package/lib/OrganizationSettings.js.map +1 -0
  59. package/lib/Providers.d.ts +24 -0
  60. package/lib/Providers.d.ts.map +1 -0
  61. package/lib/Providers.js +42 -0
  62. package/lib/Providers.js.map +1 -0
  63. package/lib/index.d.ts +14 -0
  64. package/lib/index.d.ts.map +1 -0
  65. package/lib/index.js +14 -0
  66. package/lib/index.js.map +1 -0
  67. package/package.json +54 -3
  68. package/src/ApiToken.ts +207 -0
  69. package/src/AuthProvider.ts +113 -0
  70. package/src/Connect.ts +103 -0
  71. package/src/Contributions.ts +9 -0
  72. package/src/Credentials.ts +112 -0
  73. package/src/Database.ts +519 -0
  74. package/src/DatabaseToken.ts +213 -0
  75. package/src/Group.ts +269 -0
  76. package/src/GroupToken.ts +127 -0
  77. package/src/Hrana.ts +184 -0
  78. package/src/ManageDatabases.ts +195 -0
  79. package/src/OrganizationInvite.ts +118 -0
  80. package/src/OrganizationMember.ts +122 -0
  81. package/src/OrganizationSettings.ts +145 -0
  82. package/src/Providers.ts +67 -0
  83. package/src/index.ts +20 -0
@@ -0,0 +1,519 @@
1
+ import {
2
+ diffMigrations,
3
+ migrationsAttrs,
4
+ migrationsInputOf,
5
+ runMigrations,
6
+ stampedOf,
7
+ type MigrationsInput,
8
+ } from "@alchemy.run/sql/Migrations";
9
+ import { applySqlImports, hashImports, splitSqlStatements } from "@alchemy.run/sql/SqlFile";
10
+ import * as turso from "@distilled.cloud/turso/turso";
11
+ import { Unowned } from "alchemy/AdoptPolicy";
12
+ import { deepEqual, isResolved } from "alchemy/Diff";
13
+ import { createPhysicalName } from "alchemy/PhysicalName";
14
+ import * as Provider from "alchemy/Provider";
15
+ import { Resource } from "alchemy/Resource";
16
+ import * as Data from "effect/Data";
17
+ import * as Effect from "effect/Effect";
18
+ import * as HttpClient from "effect/http/HttpClient";
19
+ import * as Redacted from "effect/Redacted";
20
+ import { organization } from "./Credentials.ts";
21
+ import * as Hrana from "./Hrana.ts";
22
+ import type { Providers } from "./Providers.ts";
23
+
24
+ export class DatabaseUnexpectedResponse extends Data.TaggedError("DatabaseUnexpectedResponse")<{
25
+ readonly message: string;
26
+ readonly cause?: unknown;
27
+ }> {}
28
+
29
+ export interface DatabaseSeed {
30
+ /**
31
+ * `"database"` copies an existing database (optionally at a point in
32
+ * time). `"database_upload"` creates an empty database awaiting an upload
33
+ * of a SQLite file.
34
+ */
35
+ type: "database" | "database_upload";
36
+ /** Name of the source database when `type` is `"database"`. */
37
+ name?: string;
38
+ /**
39
+ * ISO 8601 recovery point within the source database's retention window
40
+ * (24 hours, or 30 days on the Scaler plan).
41
+ */
42
+ timestamp?: string;
43
+ }
44
+
45
+ export interface DatabaseEncryption {
46
+ /**
47
+ * Base64-encoded encryption key: 32 bytes for `aes256gcm`,
48
+ * `chacha20poly1305`, and the `aegis256` variants; 16 bytes for
49
+ * `aes128gcm` and the `aegis128l` variants.
50
+ */
51
+ key: Redacted.Redacted<string>;
52
+ /** Encryption cipher. */
53
+ cipher:
54
+ | "aes256gcm"
55
+ | "aes128gcm"
56
+ | "chacha20poly1305"
57
+ | "aegis128l"
58
+ | "aegis128x2"
59
+ | "aegis128x4"
60
+ | "aegis256"
61
+ | "aegis256x2"
62
+ | "aegis256x4"
63
+ | (string & {});
64
+ }
65
+
66
+ export interface DatabaseProps {
67
+ /**
68
+ * Database name, unique within the organization. Lowercase letters,
69
+ * numbers, and dashes, at most 64 characters. If omitted, a unique name
70
+ * is generated from the stack, stage, and logical ID. Changing it
71
+ * replaces the database.
72
+ */
73
+ name?: string;
74
+ /**
75
+ * Name of the group the database is created in (e.g. `group.name`).
76
+ * Changing it replaces the database.
77
+ */
78
+ group: string;
79
+ /**
80
+ * Seed the new database from an existing database or a pending upload.
81
+ * Only used when the database is created.
82
+ */
83
+ seed?: DatabaseSeed;
84
+ /**
85
+ * Encrypt the database at rest with your own key. Only used when the
86
+ * database is created; changing it replaces the database. Requires a Pro
87
+ * or Enterprise plan.
88
+ */
89
+ encryption?: DatabaseEncryption;
90
+ /**
91
+ * Maximum size of the database, in bytes or with a unit (e.g. `"256mb"`,
92
+ * `"1gb"`). When omitted, the current limit is left unchanged. Only
93
+ * legacy databases support it — Turso rejects it for databases created on
94
+ * the current platform ("size_limit is not supported for db-api
95
+ * controlled databases").
96
+ */
97
+ sizeLimit?: string;
98
+ /**
99
+ * Allow other databases to be attached to this one with `ATTACH`. Not
100
+ * supported on AWS-hosted databases, which reject the setting.
101
+ * @default false
102
+ */
103
+ allowAttach?: boolean;
104
+ /**
105
+ * Reject every read query.
106
+ * @default false
107
+ */
108
+ blockReads?: boolean;
109
+ /**
110
+ * Reject every write query.
111
+ * @default false
112
+ */
113
+ blockWrites?: boolean;
114
+ /**
115
+ * Prevent the database from being deleted. While enabled, destroying or
116
+ * replacing the database fails; set it to `false` and deploy first.
117
+ * @default false
118
+ */
119
+ deleteProtection?: boolean;
120
+ /**
121
+ * IP addresses and CIDR blocks allowed to connect. Empty or omitted means
122
+ * no IP restriction. Requires a paid plan.
123
+ */
124
+ allowedIps?: string[];
125
+ /**
126
+ * AWS VPC endpoint IDs (`vpce-...`) allowed to connect. Empty or omitted
127
+ * means no VPC endpoint restriction. Requires a paid plan.
128
+ */
129
+ allowedAwsVpcIds?: string[];
130
+ /**
131
+ * SQL migrations to apply on deploy. Accepts a directory path, a
132
+ * `Drizzle.Schema` resource, or `{ dir, table? }`.
133
+ *
134
+ * Bookkeeping lives in Alchemy's `__alchemy_migrations` table. A database
135
+ * previously migrated by drizzle-kit is adopted by a one-way conversion on
136
+ * first deploy.
137
+ */
138
+ migrations?: MigrationsInput;
139
+ /**
140
+ * Paths to additional `.sql` files to apply after migrations. Each file is
141
+ * hashed; only files whose contents change are re-applied.
142
+ */
143
+ importFiles?: string[];
144
+ }
145
+
146
+ export interface DatabaseAttributes {
147
+ /** Database UUID. */
148
+ dbId: string;
149
+ /** Database name. */
150
+ name: string;
151
+ /** The organization slug that owns the database. */
152
+ organization: string;
153
+ /** Name of the group the database belongs to. */
154
+ group: string;
155
+ /** DNS hostname for libSQL and HTTP connections. */
156
+ hostname: string;
157
+ /** libSQL connection URL (`libsql://<hostname>`), for `@libsql/client`. */
158
+ url: string;
159
+ /** HTTPS URL (`https://<hostname>`) of the Hrana-over-HTTP endpoint. */
160
+ httpUrl: string;
161
+ /** Primary location of the database. */
162
+ primaryRegion: string;
163
+ /** Every location the database runs in. */
164
+ regions: string[];
165
+ /** Maximum database size, or `undefined` when unlimited. */
166
+ sizeLimit: string | undefined;
167
+ /** Whether other databases may be attached. */
168
+ allowAttach: boolean;
169
+ /** Whether reads are blocked. */
170
+ blockReads: boolean;
171
+ /** Whether writes are blocked. */
172
+ blockWrites: boolean;
173
+ /** Whether delete protection is enabled. */
174
+ deleteProtection: boolean;
175
+ /** IP addresses and CIDR blocks allowed to connect. */
176
+ allowedIps: string[];
177
+ /** AWS VPC endpoint IDs allowed to connect. */
178
+ allowedAwsVpcIds: string[];
179
+ /** Directory containing migration files, if configured. */
180
+ migrationsDir: string | undefined;
181
+ /** Table used to track applied migrations, if configured. */
182
+ migrationsTable: string | undefined;
183
+ /** Content hashes for the last applied migration files. */
184
+ migrationsHashes: Record<string, string>;
185
+ /** Content hashes for the last applied import files. */
186
+ importHashes: Record<string, string>;
187
+ }
188
+
189
+ export type Database = Resource<
190
+ "Turso.Database",
191
+ DatabaseProps,
192
+ DatabaseAttributes,
193
+ never,
194
+ Providers
195
+ >;
196
+
197
+ /**
198
+ * A Turso database: a SQLite database that lives in a {@link Group}.
199
+ * Connect to it from a Worker or Lambda with {@link Connect}, or create a
200
+ * {@link DatabaseToken} for other apps.
201
+ * @see https://docs.turso.tech/concepts#databases
202
+ *
203
+ * ### Creating a Database
204
+ * **Example:** Database in a group
205
+ * ```typescript
206
+ * const group = yield* Turso.Group("Group", { location: "aws-us-east-1" });
207
+ * const db = yield* Turso.Database("Db", { group: group.name });
208
+ * ```
209
+ *
210
+ * **Example:** Database with delete protection and an IP allowlist
211
+ * ```typescript
212
+ * const db = yield* Turso.Database("Db", {
213
+ * group: group.name,
214
+ * deleteProtection: true,
215
+ * allowedIps: ["203.0.113.0/24"],
216
+ * });
217
+ * ```
218
+ *
219
+ * ### Branching
220
+ * **Example:** Copy an existing database
221
+ * ```typescript
222
+ * const preview = yield* Turso.Database("Preview", {
223
+ * group: group.name,
224
+ * seed: { type: "database", name: db.name },
225
+ * });
226
+ * ```
227
+ *
228
+ * ### Migrations
229
+ * Point `migrations` at a folder of `.sql` files (or a `Drizzle.Schema`).
230
+ * Pending migrations are applied in order on each deploy.
231
+ *
232
+ * **Example:** Apply migrations from a directory
233
+ * ```typescript
234
+ * const db = yield* Turso.Database("Db", {
235
+ * group: group.name,
236
+ * migrations: "./migrations",
237
+ * });
238
+ * ```
239
+ *
240
+ * **Example:** Seed with SQL files
241
+ * ```typescript
242
+ * const db = yield* Turso.Database("Db", {
243
+ * group: group.name,
244
+ * importFiles: ["./seed/users.sql"],
245
+ * });
246
+ * ```
247
+ *
248
+ * ### Querying from a Worker
249
+ * **Example:** Drizzle over a Turso database
250
+ * ```typescript
251
+ * const conn = yield* Turso.Connect(Db);
252
+ * const db = yield* Drizzle.LibSQL(conn);
253
+ * ```
254
+ *
255
+ * @resource
256
+ * @product Database
257
+ */
258
+ export const Database = Resource<Database>("Turso.Database");
259
+
260
+ const databaseName = (id: string, name: string | undefined) =>
261
+ name
262
+ ? Effect.succeed(name)
263
+ : createPhysicalName({ id, lowercase: true, maxLength: 32, delimiter: "-" });
264
+
265
+ const rootDir = Effect.sync(() => process.cwd());
266
+
267
+ /**
268
+ * Run `use` against a short-lived (10 minute) full-access database token,
269
+ * for deploy-time SQL (migrations and imports).
270
+ */
271
+ const withConnection = <A, E, R>(
272
+ org: string,
273
+ database: { name: string; hostname: string },
274
+ use: (connection: Hrana.HranaConnection, client: HttpClient.HttpClient) => Effect.Effect<A, E, R>,
275
+ ) =>
276
+ Effect.gen(function* () {
277
+ const client = yield* HttpClient.HttpClient;
278
+ const { jwt } = yield* turso.createDatabaseToken({
279
+ organizationSlug: org,
280
+ databaseName: database.name,
281
+ expiration: "10m",
282
+ authorization: "full-access",
283
+ });
284
+ if (!jwt) {
285
+ return yield* new DatabaseUnexpectedResponse({
286
+ message: `Turso returned no token for ${database.name}`,
287
+ });
288
+ }
289
+ return yield* use({ url: `https://${database.hostname}`, authToken: jwt }, client);
290
+ });
291
+
292
+ export const DatabaseProvider = () =>
293
+ Provider.effect(
294
+ Database,
295
+ Effect.gen(function* () {
296
+ const observe = (org: string, name: string) =>
297
+ turso.getDatabase({ organizationSlug: org, databaseName: name }).pipe(
298
+ Effect.map((res) => res.database),
299
+ Effect.catchTag("NotFound", () => Effect.undefined),
300
+ );
301
+
302
+ const toAttrs = (
303
+ org: string,
304
+ db: turso.Database,
305
+ config: turso.DatabaseConfigurationResponse,
306
+ rest: Pick<
307
+ DatabaseAttributes,
308
+ "migrationsDir" | "migrationsTable" | "migrationsHashes" | "importHashes"
309
+ >,
310
+ ): DatabaseAttributes => ({
311
+ dbId: db.DbId ?? "",
312
+ name: db.Name!,
313
+ organization: org,
314
+ group: db.group ?? "",
315
+ hostname: db.Hostname ?? "",
316
+ url: `libsql://${db.Hostname}`,
317
+ httpUrl: `https://${db.Hostname}`,
318
+ primaryRegion: db.primaryRegion ?? "",
319
+ regions: db.regions ?? [],
320
+ sizeLimit: config.size_limit || undefined,
321
+ allowAttach: config.allow_attach ?? false,
322
+ blockReads: config.block_reads ?? false,
323
+ blockWrites: config.block_writes ?? false,
324
+ deleteProtection: config.delete_protection ?? false,
325
+ allowedIps: config.allowed_ips ?? [],
326
+ allowedAwsVpcIds: config.allowed_aws_vpc_ids ?? [],
327
+ ...rest,
328
+ });
329
+
330
+ const getConfig = (org: string, name: string) =>
331
+ turso.getDatabaseConfiguration({ organizationSlug: org, databaseName: name });
332
+
333
+ return {
334
+ stables: ["dbId", "name", "organization", "group", "hostname", "url", "httpUrl"],
335
+ diff: Effect.fn(function* ({ id, olds, news, output }) {
336
+ if (!isResolved(news)) return undefined;
337
+ const name = output?.name ?? (yield* databaseName(id, olds?.name));
338
+ if (news.name !== undefined && news.name !== name) {
339
+ return { action: "replace" } as const;
340
+ }
341
+ if (news.group !== (output?.group ?? olds?.group)) {
342
+ return { action: "replace", deleteFirst: news.name !== undefined } as const;
343
+ }
344
+ if (
345
+ news.encryption?.cipher !== olds?.encryption?.cipher ||
346
+ (news.encryption &&
347
+ olds?.encryption &&
348
+ Redacted.value(news.encryption.key) !== Redacted.value(olds.encryption.key))
349
+ ) {
350
+ return { action: "replace", deleteFirst: news.name !== undefined } as const;
351
+ }
352
+ if (yield* diffMigrations({ news, output })) {
353
+ return { action: "update" } as const;
354
+ }
355
+ if (news.importFiles?.length) {
356
+ const hashes = yield* hashImports(news.importFiles, yield* rootDir);
357
+ if (!deepEqual(hashes, output?.importHashes ?? {})) {
358
+ return { action: "update" } as const;
359
+ }
360
+ }
361
+ }),
362
+ read: Effect.fn(function* ({ id, olds, output, previous }) {
363
+ const org = output?.organization ?? (yield* organization);
364
+ const name = output?.name ?? (yield* databaseName(id, olds?.name));
365
+ const db = yield* observe(org, name);
366
+ if (!db) return undefined;
367
+ const attrs = toAttrs(org, db, yield* getConfig(org, name), {
368
+ migrationsDir: output?.migrationsDir,
369
+ migrationsTable: output?.migrationsTable,
370
+ migrationsHashes: output?.migrationsHashes ?? {},
371
+ importHashes: output?.importHashes ?? {},
372
+ });
373
+ // Turso has no tags: a database found under an explicit,
374
+ // user-chosen name without prior state may belong to someone else — unless it
375
+ // is the generation a delete-first replacement is about to free.
376
+ const ours =
377
+ output !== undefined ||
378
+ olds?.name === undefined ||
379
+ (previous?.organization === org && previous.name === name);
380
+ return ours ? attrs : Unowned(attrs);
381
+ }),
382
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
383
+ const org = output?.organization ?? (yield* organization);
384
+ const name = output?.name ?? (yield* databaseName(id, news.name));
385
+
386
+ // Observe
387
+ let db = yield* observe(org, name);
388
+
389
+ // Ensure
390
+ if (!db) {
391
+ yield* session.note(`Creating database ${name}...`);
392
+ yield* turso
393
+ .createDatabase({
394
+ organizationSlug: org,
395
+ name,
396
+ group: news.group,
397
+ seed: news.seed,
398
+ size_limit: news.sizeLimit,
399
+ remote_encryption: news.encryption
400
+ ? {
401
+ encryption_key: Redacted.value(news.encryption.key),
402
+ encryption_cipher: news.encryption.cipher,
403
+ }
404
+ : undefined,
405
+ })
406
+ .pipe(Effect.catchTag("Conflict", () => Effect.void));
407
+ db = yield* observe(org, name);
408
+ if (!db) {
409
+ return yield* new DatabaseUnexpectedResponse({
410
+ message: `Turso database ${name} vanished after create`,
411
+ });
412
+ }
413
+ }
414
+
415
+ // Sync configuration — diff observed settings against desired
416
+ // and PATCH only the fields that differ.
417
+ let config = yield* getConfig(org, name);
418
+ const patch: Omit<turso.UpdateDatabaseConfigurationRequest, "organizationSlug"> = {
419
+ databaseName: name,
420
+ };
421
+ let dirty = false;
422
+ const set = <K extends keyof typeof patch>(
423
+ key: K,
424
+ observed: unknown,
425
+ desired: (typeof patch)[K],
426
+ ) => {
427
+ if (!deepEqual(observed, desired)) {
428
+ patch[key] = desired;
429
+ dirty = true;
430
+ }
431
+ };
432
+ if (news.sizeLimit !== undefined) set("size_limit", config.size_limit, news.sizeLimit);
433
+ // AWS-hosted databases reject `allow_attach` outright, so it is
434
+ // only sent when explicitly configured.
435
+ if (news.allowAttach !== undefined) {
436
+ set("allow_attach", config.allow_attach ?? false, news.allowAttach);
437
+ }
438
+ set("block_reads", config.block_reads ?? false, news.blockReads ?? false);
439
+ set("block_writes", config.block_writes ?? false, news.blockWrites ?? false);
440
+ set(
441
+ "delete_protection",
442
+ config.delete_protection ?? false,
443
+ news.deleteProtection ?? false,
444
+ );
445
+ set("allowed_ips", config.allowed_ips ?? [], news.allowedIps ?? []);
446
+ set("allowed_aws_vpc_ids", config.allowed_aws_vpc_ids ?? [], news.allowedAwsVpcIds ?? []);
447
+ if (dirty) {
448
+ config = yield* turso.updateDatabaseConfiguration({ organizationSlug: org, ...patch });
449
+ }
450
+
451
+ // Sync schema — migrations, then SQL imports.
452
+ const target = { name, hostname: db.Hostname! };
453
+ const migrationsInput = migrationsInputOf(news);
454
+ const migrations = migrationsInput
455
+ ? yield* runMigrations({
456
+ input: migrationsInput,
457
+ stamped: stampedOf(output),
458
+ withExecutor: (apply) =>
459
+ withConnection(org, target, (connection, client) =>
460
+ apply(Hrana.makeMigrationExecutor(connection, client)),
461
+ ),
462
+ })
463
+ : undefined;
464
+
465
+ const importHashes = yield* applySqlImports({
466
+ importFiles: news.importFiles,
467
+ rootDir: yield* rootDir,
468
+ previous: output?.importHashes,
469
+ apply: (file) =>
470
+ session
471
+ .note(`Importing ${file.id}...`)
472
+ .pipe(
473
+ Effect.andThen(
474
+ withConnection(org, target, (connection, client) =>
475
+ Hrana.transaction(connection, splitSqlStatements(file.sql)).pipe(
476
+ Effect.provideService(HttpClient.HttpClient, client),
477
+ ),
478
+ ),
479
+ ),
480
+ ),
481
+ });
482
+
483
+ return toAttrs(org, db, config, {
484
+ ...migrationsAttrs({ input: migrationsInput, run: migrations, output }),
485
+ importHashes,
486
+ });
487
+ }),
488
+ delete: Effect.fn(function* ({ output }) {
489
+ yield* turso
490
+ .deleteDatabase({ organizationSlug: output.organization, databaseName: output.name })
491
+ .pipe(Effect.catchTag("NotFound", () => Effect.void));
492
+ }),
493
+ list: Effect.fn(function* () {
494
+ const org = yield* organization;
495
+ const { databases } = yield* turso.listDatabases({ organizationSlug: org });
496
+ return (databases ?? [])
497
+ .filter((db) => db.Name !== undefined)
498
+ .map((db) =>
499
+ toAttrs(
500
+ org,
501
+ db,
502
+ {
503
+ allow_attach: db.allow_attach,
504
+ block_reads: db.block_reads,
505
+ block_writes: db.block_writes,
506
+ delete_protection: db.delete_protection,
507
+ },
508
+ {
509
+ migrationsDir: undefined,
510
+ migrationsTable: undefined,
511
+ migrationsHashes: {},
512
+ importHashes: {},
513
+ },
514
+ ),
515
+ );
516
+ }),
517
+ };
518
+ }),
519
+ );