@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.
- package/LICENSE +201 -0
- package/README.md +68 -4
- package/lib/ApiToken.d.ts +82 -0
- package/lib/ApiToken.d.ts.map +1 -0
- package/lib/ApiToken.js +128 -0
- package/lib/ApiToken.js.map +1 -0
- package/lib/AuthProvider.d.ts +36 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +85 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Connect.d.ts +79 -0
- package/lib/Connect.d.ts.map +1 -0
- package/lib/Connect.js +22 -0
- package/lib/Connect.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/Credentials.d.ts +39 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +76 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Database.d.ts +226 -0
- package/lib/Database.d.ts.map +1 -0
- package/lib/Database.js +277 -0
- package/lib/Database.js.map +1 -0
- package/lib/DatabaseToken.d.ts +104 -0
- package/lib/DatabaseToken.d.ts.map +1 -0
- package/lib/DatabaseToken.js +133 -0
- package/lib/DatabaseToken.js.map +1 -0
- package/lib/Group.d.ts +102 -0
- package/lib/Group.d.ts.map +1 -0
- package/lib/Group.js +171 -0
- package/lib/Group.js.map +1 -0
- package/lib/GroupToken.d.ts +57 -0
- package/lib/GroupToken.d.ts.map +1 -0
- package/lib/GroupToken.js +88 -0
- package/lib/GroupToken.js.map +1 -0
- package/lib/Hrana.d.ts +42 -0
- package/lib/Hrana.d.ts.map +1 -0
- package/lib/Hrana.js +109 -0
- package/lib/Hrana.js.map +1 -0
- package/lib/ManageDatabases.d.ts +73 -0
- package/lib/ManageDatabases.d.ts.map +1 -0
- package/lib/ManageDatabases.js +81 -0
- package/lib/ManageDatabases.js.map +1 -0
- package/lib/OrganizationInvite.d.ts +46 -0
- package/lib/OrganizationInvite.d.ts.map +1 -0
- package/lib/OrganizationInvite.js +72 -0
- package/lib/OrganizationInvite.js.map +1 -0
- package/lib/OrganizationMember.d.ts +47 -0
- package/lib/OrganizationMember.d.ts.map +1 -0
- package/lib/OrganizationMember.js +81 -0
- package/lib/OrganizationMember.js.map +1 -0
- package/lib/OrganizationSettings.d.ts +52 -0
- package/lib/OrganizationSettings.d.ts.map +1 -0
- package/lib/OrganizationSettings.js +93 -0
- package/lib/OrganizationSettings.js.map +1 -0
- package/lib/Providers.d.ts +24 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +42 -0
- package/lib/Providers.js.map +1 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/package.json +54 -3
- package/src/ApiToken.ts +207 -0
- package/src/AuthProvider.ts +113 -0
- package/src/Connect.ts +103 -0
- package/src/Contributions.ts +9 -0
- package/src/Credentials.ts +112 -0
- package/src/Database.ts +519 -0
- package/src/DatabaseToken.ts +213 -0
- package/src/Group.ts +269 -0
- package/src/GroupToken.ts +127 -0
- package/src/Hrana.ts +184 -0
- package/src/ManageDatabases.ts +195 -0
- package/src/OrganizationInvite.ts +118 -0
- package/src/OrganizationMember.ts +122 -0
- package/src/OrganizationSettings.ts +145 -0
- package/src/Providers.ts +67 -0
- package/src/index.ts +20 -0
package/src/Database.ts
ADDED
|
@@ -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
|
+
);
|