@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.
- package/lib/ApiToken.d.ts +9 -0
- package/lib/ApiToken.d.ts.map +1 -1
- package/lib/ApiToken.js +10 -2
- package/lib/ApiToken.js.map +1 -1
- package/lib/AuthProvider.d.ts +1 -1
- package/lib/AuthProvider.d.ts.map +1 -1
- package/lib/AuthProvider.js +1 -1
- package/lib/AuthProvider.js.map +1 -1
- package/lib/Connect.d.ts.map +1 -1
- package/lib/Connect.js.map +1 -1
- package/lib/Contributions.d.ts +1 -1
- package/lib/Contributions.d.ts.map +1 -1
- package/lib/Contributions.js.map +1 -1
- package/lib/Credentials.d.ts +1 -1
- package/lib/Credentials.d.ts.map +1 -1
- package/lib/Credentials.js +11 -15
- package/lib/Credentials.js.map +1 -1
- package/lib/Database.d.ts +36 -1
- package/lib/Database.d.ts.map +1 -1
- package/lib/Database.js +49 -20
- package/lib/Database.js.map +1 -1
- package/lib/DatabaseToken.d.ts +9 -0
- package/lib/DatabaseToken.d.ts.map +1 -1
- package/lib/DatabaseToken.js +6 -1
- package/lib/DatabaseToken.js.map +1 -1
- package/lib/Group.d.ts +29 -1
- package/lib/Group.d.ts.map +1 -1
- package/lib/Group.js +30 -5
- package/lib/Group.js.map +1 -1
- package/lib/GroupToken.d.ts +9 -0
- package/lib/GroupToken.d.ts.map +1 -1
- package/lib/GroupToken.js +6 -1
- package/lib/GroupToken.js.map +1 -1
- package/lib/Hrana.d.ts.map +1 -1
- package/lib/Hrana.js +5 -1
- package/lib/Hrana.js.map +1 -1
- package/lib/ManageDatabases.d.ts.map +1 -1
- package/lib/ManageDatabases.js +1 -1
- package/lib/ManageDatabases.js.map +1 -1
- package/lib/OrganizationInvite.d.ts.map +1 -1
- package/lib/OrganizationInvite.js.map +1 -1
- package/lib/OrganizationMember.d.ts.map +1 -1
- package/lib/OrganizationMember.js +1 -1
- package/lib/OrganizationMember.js.map +1 -1
- package/lib/OrganizationSettings.d.ts.map +1 -1
- package/lib/OrganizationSettings.js.map +1 -1
- package/lib/Providers.d.ts +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +9 -9
- package/src/ApiToken.ts +12 -2
- package/src/AuthProvider.ts +1 -1
- package/src/Credentials.ts +22 -49
- package/src/Database.ts +79 -24
- package/src/DatabaseToken.ts +11 -1
- package/src/Group.ts +52 -6
- package/src/GroupToken.ts +9 -1
- package/src/Hrana.ts +3 -1
- package/src/ManageDatabases.ts +1 -1
- package/src/OrganizationMember.ts +1 -1
package/src/Credentials.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 {
|
|
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*
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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*
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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 }),
|
package/src/DatabaseToken.ts
CHANGED
|
@@ -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*
|
|
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<
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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*
|
|
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*
|
|
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
|
|
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 => {
|
package/src/ManageDatabases.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
67
|
+
Effect.catchTag("NotFound", () => Effect.undefined),
|
|
68
68
|
);
|
|
69
69
|
|
|
70
70
|
return {
|