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

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 +73 -0
  4. package/lib/ApiToken.d.ts.map +1 -0
  5. package/lib/ApiToken.js +120 -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 +80 -0
  22. package/lib/Credentials.js.map +1 -0
  23. package/lib/Database.d.ts +217 -0
  24. package/lib/Database.d.ts.map +1 -0
  25. package/lib/Database.js +271 -0
  26. package/lib/Database.js.map +1 -0
  27. package/lib/DatabaseToken.d.ts +95 -0
  28. package/lib/DatabaseToken.d.ts.map +1 -0
  29. package/lib/DatabaseToken.js +128 -0
  30. package/lib/DatabaseToken.js.map +1 -0
  31. package/lib/Group.d.ts +93 -0
  32. package/lib/Group.d.ts.map +1 -0
  33. package/lib/Group.js +162 -0
  34. package/lib/Group.js.map +1 -0
  35. package/lib/GroupToken.d.ts +48 -0
  36. package/lib/GroupToken.d.ts.map +1 -0
  37. package/lib/GroupToken.js +83 -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 +105 -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 +197 -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 +139 -0
  73. package/src/Database.ts +504 -0
  74. package/src/DatabaseToken.ts +203 -0
  75. package/src/Group.ts +256 -0
  76. package/src/GroupToken.ts +119 -0
  77. package/src/Hrana.ts +182 -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,195 @@
1
+ import { Credentials, DEFAULT_API_BASE_URL } from "@distilled.cloud/turso/Credentials";
2
+ import * as turso from "@distilled.cloud/turso/turso";
3
+ import * as Binding from "alchemy/Binding";
4
+ import type { RuntimeContext } from "alchemy/RuntimeContext";
5
+ import * as Effect from "effect/Effect";
6
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
7
+ import type * as HttpClient from "effect/http/HttpClient";
8
+ import * as Layer from "effect/Layer";
9
+ import { ApiToken } from "./ApiToken.ts";
10
+ import type { ConnectClient } from "./Connect.ts";
11
+ import type { DatabaseSeed } from "./Database.ts";
12
+ import type { Group } from "./Group.ts";
13
+ import { GroupToken } from "./GroupToken.ts";
14
+
15
+ /** A database in the managed group. */
16
+ export interface ManagedDatabase {
17
+ /** Database name. */
18
+ name: string;
19
+ /** Database UUID. */
20
+ dbId: string;
21
+ /** DNS hostname of the database. */
22
+ hostname: string;
23
+ /** libSQL URL (`libsql://<hostname>`). */
24
+ url: string;
25
+ }
26
+
27
+ export interface CreateManagedDatabaseOptions {
28
+ /** Seed the new database from an existing database (e.g. a template). */
29
+ seed?: DatabaseSeed;
30
+ /** Maximum size of the database, e.g. `"256mb"`. */
31
+ sizeLimit?: string;
32
+ }
33
+
34
+ export interface ManageDatabasesClient {
35
+ /** Create a database in the group. */
36
+ create(
37
+ name: string,
38
+ options?: CreateManagedDatabaseOptions,
39
+ ): Effect.Effect<ManagedDatabase, turso.CreateDatabaseError, RuntimeContext>;
40
+ /** Look up a database by name; `undefined` when it doesn't exist. */
41
+ get(
42
+ name: string,
43
+ ): Effect.Effect<ManagedDatabase | undefined, turso.GetDatabaseError, RuntimeContext>;
44
+ /** List every database in the group. */
45
+ list(): Effect.Effect<ManagedDatabase[], turso.ListDatabasesError, RuntimeContext>;
46
+ /** Delete a database. Deleting a missing database succeeds. */
47
+ delete(name: string): Effect.Effect<void, turso.DeleteDatabaseError, RuntimeContext>;
48
+ /**
49
+ * libSQL connection accessors for a database in the group, authorized by
50
+ * a group-wide SQL token. Pass to `SQL.LibSQL` or `Drizzle.LibSQL`.
51
+ */
52
+ connect(name: string): ConnectClient;
53
+ }
54
+
55
+ /**
56
+ * Let a Worker create, list, delete, and query databases in one Turso
57
+ * {@link Group}. Use it to give each customer their own database.
58
+ *
59
+ * The Worker gets an {@link ApiToken} that only works on that group, and a
60
+ * {@link GroupToken} for running SQL. It can't touch databases in any other
61
+ * group, and your own Turso API token stays on your machine.
62
+ *
63
+ * ### Database per tenant
64
+ * **Example:** Create a tenant database on signup and query it
65
+ * ```typescript
66
+ * Effect.gen(function* () {
67
+ * const tenants = yield* Turso.ManageDatabases(Tenants);
68
+ * return {
69
+ * fetch: Effect.gen(function* () {
70
+ * const db = yield* tenants.create("acme");
71
+ * const sql = yield* SQL.LibSQL(tenants.connect(db.name));
72
+ * yield* sql`CREATE TABLE IF NOT EXISTS notes (body TEXT)`;
73
+ * return HttpServerResponse.text(db.url);
74
+ * }),
75
+ * };
76
+ * }).pipe(Effect.provide(Turso.ManageDatabasesHttp));
77
+ * ```
78
+ *
79
+ * @binding
80
+ * @product Group
81
+ */
82
+ export interface ManageDatabases extends Binding.Service<
83
+ ManageDatabases,
84
+ "Turso.ManageDatabases",
85
+ (group: Group) => Effect.Effect<ManageDatabasesClient>
86
+ > {}
87
+
88
+ export const ManageDatabases = Binding.Service<ManageDatabases>("Turso.ManageDatabases");
89
+
90
+ /** Implementation over the Turso Platform API, usable from any Platform host. */
91
+ export const ManageDatabasesHttp = Layer.effect(
92
+ ManageDatabases,
93
+ Effect.gen(function* () {
94
+ const PlatformToken = yield* ApiToken;
95
+ const SqlToken = yield* GroupToken;
96
+ return Effect.fn(function* (group: Group) {
97
+ const host = yield* Binding.Host;
98
+ const prefix = `${host?.LogicalId ?? ""}${group.LogicalId}`;
99
+ const apiToken = yield* PlatformToken(`${prefix}ManageToken`, {
100
+ group: group.name,
101
+ scopes: ["read", "db:create", "db:delete", "db:configure"],
102
+ });
103
+ const sqlToken = yield* SqlToken(`${prefix}SqlToken`, {
104
+ group: group.name,
105
+ });
106
+ const apiKey = yield* apiToken.token;
107
+ const authToken = yield* sqlToken.token;
108
+ const organization = yield* group.organization;
109
+ const groupName = yield* group.name;
110
+ const location = yield* group.location;
111
+
112
+ const authorize = <A, E>(
113
+ effect: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
114
+ ): Effect.Effect<A, E, RuntimeContext> =>
115
+ effect.pipe(
116
+ Effect.provide(
117
+ Layer.mergeAll(
118
+ Layer.succeed(
119
+ Credentials,
120
+ Effect.map(apiKey, (key) => ({ apiKey: key, apiBaseUrl: DEFAULT_API_BASE_URL })),
121
+ ),
122
+ FetchHttpClient.layer,
123
+ ),
124
+ ),
125
+ );
126
+
127
+ const toManaged = (db: {
128
+ Name?: string;
129
+ DbId?: string;
130
+ Hostname?: string;
131
+ }): ManagedDatabase => ({
132
+ name: db.Name ?? "",
133
+ dbId: db.DbId ?? "",
134
+ hostname: db.Hostname ?? "",
135
+ url: `libsql://${db.Hostname}`,
136
+ });
137
+
138
+ const client: ManageDatabasesClient = {
139
+ create: (name, options) =>
140
+ authorize(
141
+ Effect.gen(function* () {
142
+ const { database } = yield* turso.createDatabase({
143
+ organizationSlug: yield* organization,
144
+ name,
145
+ group: yield* groupName,
146
+ seed: options?.seed,
147
+ size_limit: options?.sizeLimit,
148
+ });
149
+ return toManaged(database ?? {});
150
+ }),
151
+ ),
152
+ get: (name) =>
153
+ authorize(
154
+ Effect.gen(function* () {
155
+ const { database } = yield* turso.getDatabase({
156
+ organizationSlug: yield* organization,
157
+ databaseName: name,
158
+ });
159
+ return database && database.group === (yield* groupName)
160
+ ? toManaged(database)
161
+ : undefined;
162
+ }).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))),
163
+ ),
164
+ list: () =>
165
+ authorize(
166
+ Effect.gen(function* () {
167
+ const { databases } = yield* turso.listDatabases({
168
+ organizationSlug: yield* organization,
169
+ group: yield* groupName,
170
+ });
171
+ return (databases ?? []).map(toManaged);
172
+ }),
173
+ ),
174
+ delete: (name) =>
175
+ authorize(
176
+ Effect.gen(function* () {
177
+ yield* turso.deleteDatabase({
178
+ organizationSlug: yield* organization,
179
+ databaseName: name,
180
+ });
181
+ }).pipe(Effect.catchTag("NotFound", () => Effect.void)),
182
+ ),
183
+ connect: (name) => ({
184
+ // Turso hostnames are `<database>-<organization>.<location>.turso.io`,
185
+ // so the URL is derived without a Platform API round-trip.
186
+ url: Effect.gen(function* () {
187
+ return `libsql://${name}-${yield* organization}.${yield* location}.turso.io`;
188
+ }),
189
+ authToken,
190
+ }),
191
+ };
192
+ return client;
193
+ });
194
+ }),
195
+ );
@@ -0,0 +1,118 @@
1
+ import * as turso from "@distilled.cloud/turso/turso";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import * as Provider from "alchemy/Provider";
4
+ import { Resource } from "alchemy/Resource";
5
+ import * as Effect from "effect/Effect";
6
+ import { organization } from "./Credentials.ts";
7
+ import type { Providers } from "./Providers.ts";
8
+
9
+ /** Role granted to an invited or added organization member. */
10
+ export type MemberRole = "admin" | "member" | "viewer";
11
+
12
+ export interface OrganizationInviteProps {
13
+ /** Email address to invite. Changing it replaces the invite. */
14
+ email: string;
15
+ /**
16
+ * Role the invitee receives on accepting.
17
+ * @default "member"
18
+ */
19
+ role?: MemberRole;
20
+ }
21
+
22
+ export interface OrganizationInviteAttributes {
23
+ /** The invited email address. */
24
+ email: string;
25
+ /** The organization slug the invite is for. */
26
+ organization: string;
27
+ /** Role the invitee receives on accepting. */
28
+ role: string;
29
+ }
30
+
31
+ export type OrganizationInvite = Resource<
32
+ "Turso.OrganizationInvite",
33
+ OrganizationInviteProps,
34
+ OrganizationInviteAttributes,
35
+ never,
36
+ Providers
37
+ >;
38
+
39
+ /**
40
+ * An invitation for someone to join your Turso organization by email.
41
+ * Changing `role` re-sends the invite with the new role; destroying the
42
+ * resource withdraws a pending invite. Requires a team organization on a
43
+ * paid plan — the Starter plan rejects invites with `BadRequest`.
44
+ * @see https://docs.turso.tech/api-reference/organizations/invites/create
45
+ *
46
+ * ### Inviting Members
47
+ * **Example:** Invite a teammate as an admin
48
+ * ```typescript
49
+ * yield* Turso.OrganizationInvite("Alice", {
50
+ * email: "alice@example.com",
51
+ * role: "admin",
52
+ * });
53
+ * ```
54
+ *
55
+ * @resource
56
+ * @product Organization
57
+ */
58
+ export const OrganizationInvite = Resource<OrganizationInvite>("Turso.OrganizationInvite");
59
+
60
+ export const OrganizationInviteProvider = () =>
61
+ Provider.effect(
62
+ OrganizationInvite,
63
+ Effect.gen(function* () {
64
+ const observe = (org: string, email: string) =>
65
+ turso
66
+ .listOrganizationInvitesV2({ organizationSlug: org })
67
+ .pipe(
68
+ Effect.map(({ invites }) =>
69
+ (invites ?? []).find((i) => i.email?.toLowerCase() === email.toLowerCase()),
70
+ ),
71
+ );
72
+
73
+ const withdraw = (org: string, email: string) =>
74
+ turso.deleteOrganizationInviteByEmailV2({ organizationSlug: org, email }).pipe(
75
+ Effect.asVoid,
76
+ Effect.catchTag("NotFound", () => Effect.void),
77
+ );
78
+
79
+ return {
80
+ stables: ["email", "organization"],
81
+ diff: Effect.fn(function* ({ olds, news }) {
82
+ if (!isResolved(news)) return undefined;
83
+ if (news.email.toLowerCase() !== olds?.email.toLowerCase()) {
84
+ return { action: "replace" } as const;
85
+ }
86
+ }),
87
+ read: Effect.fn(function* ({ olds, output }) {
88
+ const org = output?.organization ?? (yield* organization);
89
+ const invite = yield* observe(org, output?.email ?? olds.email);
90
+ if (!invite?.email) return undefined;
91
+ return { email: invite.email, organization: org, role: invite.role ?? "member" };
92
+ }),
93
+ reconcile: Effect.fn(function* ({ news, output }) {
94
+ const org = output?.organization ?? (yield* organization);
95
+ const role = news.role ?? "member";
96
+
97
+ // Observe
98
+ const invite = yield* observe(org, news.email);
99
+
100
+ // Ensure / sync — invites have no update API, so a role change
101
+ // withdraws and re-sends.
102
+ if (invite?.role === role) {
103
+ return { email: invite.email ?? news.email, organization: org, role };
104
+ }
105
+ if (invite) yield* withdraw(org, news.email);
106
+ const { invited } = yield* turso.inviteOrganizationMemberV2({
107
+ organizationSlug: org,
108
+ email: news.email,
109
+ role,
110
+ });
111
+ return { email: invited?.email ?? news.email, organization: org, role };
112
+ }),
113
+ delete: Effect.fn(function* ({ output }) {
114
+ yield* withdraw(output.organization, output.email);
115
+ }),
116
+ };
117
+ }),
118
+ );
@@ -0,0 +1,122 @@
1
+ import * as turso from "@distilled.cloud/turso/turso";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import * as Provider from "alchemy/Provider";
4
+ import { Resource } from "alchemy/Resource";
5
+ import * as Effect from "effect/Effect";
6
+ import { organization } from "./Credentials.ts";
7
+ import type { MemberRole } from "./OrganizationInvite.ts";
8
+ import type { Providers } from "./Providers.ts";
9
+
10
+ export interface OrganizationMemberProps {
11
+ /** Username of an existing Turso user. Changing it replaces the member. */
12
+ username: string;
13
+ /**
14
+ * Role of the member in the organization.
15
+ * @default "member"
16
+ */
17
+ role?: MemberRole;
18
+ }
19
+
20
+ export interface OrganizationMemberAttributes {
21
+ /** Username of the member. */
22
+ username: string;
23
+ /** The organization slug. */
24
+ organization: string;
25
+ /** Role of the member. */
26
+ role: string;
27
+ /** Email of the member. */
28
+ email: string | undefined;
29
+ }
30
+
31
+ export type OrganizationMember = Resource<
32
+ "Turso.OrganizationMember",
33
+ OrganizationMemberProps,
34
+ OrganizationMemberAttributes,
35
+ never,
36
+ Providers
37
+ >;
38
+
39
+ /**
40
+ * Adds an existing Turso user to your organization with a role, and keeps
41
+ * that role in sync. Destroying the resource removes the member. Use
42
+ * {@link OrganizationInvite} for people who don't have a Turso account
43
+ * yet. Requires a team organization on a paid plan.
44
+ * @see https://docs.turso.tech/api-reference/organizations/members/add
45
+ *
46
+ * ### Managing Members
47
+ * **Example:** Add a user as a viewer
48
+ * ```typescript
49
+ * yield* Turso.OrganizationMember("Bob", {
50
+ * username: "bob",
51
+ * role: "viewer",
52
+ * });
53
+ * ```
54
+ *
55
+ * @resource
56
+ * @product Organization
57
+ */
58
+ export const OrganizationMember = Resource<OrganizationMember>("Turso.OrganizationMember");
59
+
60
+ export const OrganizationMemberProvider = () =>
61
+ Provider.effect(
62
+ OrganizationMember,
63
+ Effect.gen(function* () {
64
+ const observe = (org: string, username: string) =>
65
+ turso.getOrganizationMember({ organizationSlug: org, username }).pipe(
66
+ Effect.map(({ member }) => member),
67
+ Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
68
+ );
69
+
70
+ return {
71
+ stables: ["username", "organization"],
72
+ diff: Effect.fn(function* ({ olds, news }) {
73
+ if (!isResolved(news)) return undefined;
74
+ if (news.username !== olds?.username) return { action: "replace" } as const;
75
+ }),
76
+ read: Effect.fn(function* ({ olds, output }) {
77
+ const org = output?.organization ?? (yield* organization);
78
+ const member = yield* observe(org, output?.username ?? olds.username);
79
+ if (!member?.username) return undefined;
80
+ return {
81
+ username: member.username,
82
+ organization: org,
83
+ role: member.role ?? "member",
84
+ email: member.email,
85
+ };
86
+ }),
87
+ reconcile: Effect.fn(function* ({ news, output }) {
88
+ const org = output?.organization ?? (yield* organization);
89
+ const role = news.role ?? "member";
90
+
91
+ // Observe
92
+ let member = yield* observe(org, news.username);
93
+
94
+ // Ensure
95
+ if (!member) {
96
+ yield* turso
97
+ .addOrganizationMember({ organizationSlug: org, username: news.username, role })
98
+ .pipe(Effect.catchTag("Conflict", () => Effect.void));
99
+ member = yield* observe(org, news.username);
100
+ }
101
+
102
+ // Sync role
103
+ if (member?.role !== role) {
104
+ yield* turso.updateMemberRole({
105
+ organizationSlug: org,
106
+ username: news.username,
107
+ role,
108
+ });
109
+ }
110
+ return { username: news.username, organization: org, role, email: member?.email };
111
+ }),
112
+ delete: Effect.fn(function* ({ output }) {
113
+ yield* turso
114
+ .removeOrganizationMember({
115
+ organizationSlug: output.organization,
116
+ username: output.username,
117
+ })
118
+ .pipe(Effect.catchTag("NotFound", () => Effect.void));
119
+ }),
120
+ };
121
+ }),
122
+ );
@@ -0,0 +1,145 @@
1
+ import * as turso from "@distilled.cloud/turso/turso";
2
+ import * as Provider from "alchemy/Provider";
3
+ import { Resource } from "alchemy/Resource";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Schedule from "effect/Schedule";
6
+ import { organization } from "./Credentials.ts";
7
+ import type { Providers } from "./Providers.ts";
8
+
9
+ export interface OrganizationSettingsProps {
10
+ /** Allow usage beyond the plan's quota to be billed as overages. */
11
+ overages?: boolean;
12
+ /**
13
+ * Require every member to enable multi-factor authentication. The
14
+ * deploying user must have MFA enabled first.
15
+ */
16
+ requireMfa?: boolean;
17
+ /**
18
+ * Keep deleted databases and groups recoverable. Requires an admin or
19
+ * owner.
20
+ */
21
+ restoreEnabled?: boolean;
22
+ }
23
+
24
+ interface Settings {
25
+ overages: boolean;
26
+ requireMfa: boolean;
27
+ restoreEnabled: boolean;
28
+ }
29
+
30
+ export interface OrganizationSettingsAttributes extends Settings {
31
+ /** The organization slug. */
32
+ organization: string;
33
+ /** Settings observed before Alchemy first changed them; restored on destroy. */
34
+ original: Settings;
35
+ }
36
+
37
+ export type OrganizationSettings = Resource<
38
+ "Turso.OrganizationSettings",
39
+ OrganizationSettingsProps,
40
+ OrganizationSettingsAttributes,
41
+ never,
42
+ Providers
43
+ >;
44
+
45
+ /**
46
+ * Settings of the Turso organization your credentials act on. Only the
47
+ * settings you set are managed; destroying the resource restores the values
48
+ * observed before Alchemy first changed them.
49
+ * @see https://docs.turso.tech/api-reference/organizations/update
50
+ *
51
+ * ### Configuring the Organization
52
+ * **Example:** Enable overages and require MFA
53
+ * ```typescript
54
+ * yield* Turso.OrganizationSettings("Org", {
55
+ * overages: true,
56
+ * requireMfa: true,
57
+ * });
58
+ * ```
59
+ *
60
+ * @resource
61
+ * @product Organization
62
+ */
63
+ export const OrganizationSettings = Resource<OrganizationSettings>("Turso.OrganizationSettings");
64
+
65
+ const toSettings = (org: turso.Organization | undefined): Settings => ({
66
+ overages: org?.overages ?? false,
67
+ requireMfa: org?.require_mfa ?? false,
68
+ restoreEnabled: org?.restore_enabled ?? false,
69
+ });
70
+
71
+ export const OrganizationSettingsProvider = () =>
72
+ Provider.effect(
73
+ OrganizationSettings,
74
+ Effect.gen(function* () {
75
+ const observe = (slug: string) =>
76
+ turso
77
+ .getOrganization({ organizationSlug: slug })
78
+ .pipe(Effect.map(({ organization }) => toSettings(organization)));
79
+
80
+ // `restore_enabled` must be updated on its own (per the API).
81
+ const apply = (slug: string, observed: Settings, desired: Partial<Settings>) =>
82
+ Effect.gen(function* () {
83
+ const patch: { overages?: boolean; require_mfa?: boolean } = {};
84
+ if (desired.overages !== undefined && desired.overages !== observed.overages) {
85
+ patch.overages = desired.overages;
86
+ }
87
+ if (desired.requireMfa !== undefined && desired.requireMfa !== observed.requireMfa) {
88
+ patch.require_mfa = desired.requireMfa;
89
+ }
90
+ if (Object.keys(patch).length > 0) {
91
+ yield* turso.updateOrganization({ organizationSlug: slug, ...patch });
92
+ }
93
+ if (
94
+ desired.restoreEnabled !== undefined &&
95
+ desired.restoreEnabled !== observed.restoreEnabled
96
+ ) {
97
+ yield* turso.updateOrganization({
98
+ organizationSlug: slug,
99
+ restore_enabled: desired.restoreEnabled,
100
+ });
101
+ }
102
+ });
103
+
104
+ return {
105
+ stables: ["organization", "original"],
106
+ read: Effect.fn(function* ({ output }) {
107
+ if (!output) return undefined;
108
+ return { ...output, ...(yield* observe(output.organization)) };
109
+ }),
110
+ reconcile: Effect.fn(function* ({ news, output }) {
111
+ const slug = output?.organization ?? (yield* organization);
112
+ const observed = yield* observe(slug);
113
+ yield* apply(slug, observed, news);
114
+ // Organization reads can briefly lag a PATCH; wait until the
115
+ // settings we set are visible.
116
+ const settled = yield* observe(slug).pipe(
117
+ Effect.repeat({
118
+ schedule: Schedule.spaced("1 second"),
119
+ until: (current) =>
120
+ (news.overages === undefined || current.overages === news.overages) &&
121
+ (news.requireMfa === undefined || current.requireMfa === news.requireMfa) &&
122
+ (news.restoreEnabled === undefined ||
123
+ current.restoreEnabled === news.restoreEnabled),
124
+ times: 10,
125
+ }),
126
+ );
127
+ return {
128
+ organization: slug,
129
+ original: output?.original ?? observed,
130
+ ...settled,
131
+ };
132
+ }),
133
+ delete: Effect.fn(function* ({ olds, output }) {
134
+ const observed = yield* observe(output.organization);
135
+ // Restore only the settings this resource managed.
136
+ yield* apply(output.organization, observed, {
137
+ overages: olds.overages !== undefined ? output.original.overages : undefined,
138
+ requireMfa: olds.requireMfa !== undefined ? output.original.requireMfa : undefined,
139
+ restoreEnabled:
140
+ olds.restoreEnabled !== undefined ? output.original.restoreEnabled : undefined,
141
+ });
142
+ }),
143
+ };
144
+ }),
145
+ );
@@ -0,0 +1,67 @@
1
+ import { CredentialsStoreLive } from "alchemy/Auth/Credentials";
2
+ import { ProfileStoreLive } from "alchemy/Auth/Profile";
3
+ import * as Provider from "alchemy/Provider";
4
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
5
+ import * as Layer from "effect/Layer";
6
+ import { ApiToken, ApiTokenProvider } from "./ApiToken.ts";
7
+ import { TursoAuth } from "./AuthProvider.ts";
8
+ import * as Credentials from "./Credentials.ts";
9
+ import { Database, DatabaseProvider } from "./Database.ts";
10
+ import { DatabaseToken, DatabaseTokenProvider } from "./DatabaseToken.ts";
11
+ import { Group, GroupProvider } from "./Group.ts";
12
+ import { GroupToken, GroupTokenProvider } from "./GroupToken.ts";
13
+ import { OrganizationInvite, OrganizationInviteProvider } from "./OrganizationInvite.ts";
14
+ import { OrganizationMember, OrganizationMemberProvider } from "./OrganizationMember.ts";
15
+ import { OrganizationSettings, OrganizationSettingsProvider } from "./OrganizationSettings.ts";
16
+
17
+ /**
18
+ * Service tag bundling all Turso providers + auth + credentials. Use
19
+ * `Turso.providers()` to materialize the full layer.
20
+ */
21
+ export class Providers extends Provider.ProviderCollection<Providers>()("Turso") {}
22
+
23
+ export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
24
+
25
+ /**
26
+ * Build the complete Turso providers Layer: every Turso resource, the
27
+ * Turso AuthProvider registration (so `alchemy profile edit` can configure
28
+ * it), and credential resolution.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * Alchemy.Stack("App", { providers: Turso.providers() }, ...)
33
+ * ```
34
+ */
35
+ export const providers = () =>
36
+ Layer.effect(
37
+ Providers,
38
+ Provider.collection([
39
+ ApiToken,
40
+ Database,
41
+ DatabaseToken,
42
+ Group,
43
+ GroupToken,
44
+ OrganizationInvite,
45
+ OrganizationMember,
46
+ OrganizationSettings,
47
+ ]),
48
+ ).pipe(
49
+ Layer.provide(
50
+ Layer.mergeAll(
51
+ ApiTokenProvider(),
52
+ DatabaseProvider(),
53
+ DatabaseTokenProvider(),
54
+ GroupProvider(),
55
+ GroupTokenProvider(),
56
+ OrganizationInviteProvider(),
57
+ OrganizationMemberProvider(),
58
+ OrganizationSettingsProvider(),
59
+ ),
60
+ ),
61
+ Layer.provideMerge(Credentials.fromAuthProvider()),
62
+ Layer.provideMerge(FetchHttpClient.layer),
63
+ Layer.provideMerge(TursoAuth),
64
+ Layer.provideMerge(ProfileStoreLive),
65
+ Layer.provideMerge(CredentialsStoreLive),
66
+ Layer.orDie,
67
+ );
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export * from "./ApiToken.ts";
2
+ export * from "./AuthProvider.ts";
3
+ export * from "./Connect.ts";
4
+ export {
5
+ Credentials,
6
+ CredentialsFromEnv,
7
+ DEFAULT_API_BASE_URL,
8
+ fromAuthProvider,
9
+ fromToken,
10
+ Organization,
11
+ } from "./Credentials.ts";
12
+ export * from "./Database.ts";
13
+ export * from "./DatabaseToken.ts";
14
+ export * from "./Group.ts";
15
+ export * from "./GroupToken.ts";
16
+ export * from "./ManageDatabases.ts";
17
+ export * from "./OrganizationInvite.ts";
18
+ export * from "./OrganizationMember.ts";
19
+ export * from "./OrganizationSettings.ts";
20
+ export * from "./Providers.ts";