@samebase/alchemy-convex 0.9.0 → 0.11.0
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/README.md +55 -3
- package/lib/index.d.ts +26 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +53 -8
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/src/Component.ts +54 -0
- package/src/Deployment.ts +20 -6
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samebase/alchemy-convex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The Convex plane for Alchemy v2: projects, deployments, environment variables, and the push of the functions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -51,24 +51,25 @@
|
|
|
51
51
|
"@convex-dev/platform": "0.1.11"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@alchemy.run/cloudflare": "2.0.0-beta.84",
|
|
54
55
|
"@arethetypeswrong/core": "0.18.5",
|
|
55
|
-
"@effect/platform-node": "4.0.
|
|
56
|
-
"@effect/vitest": "4.0.
|
|
56
|
+
"@effect/platform-node": "4.0.5",
|
|
57
|
+
"@effect/vitest": "4.0.5",
|
|
57
58
|
"@types/node": "24.19.2",
|
|
58
|
-
"alchemy": "2.0.0-beta.
|
|
59
|
+
"alchemy": "2.0.0-beta.84",
|
|
59
60
|
"convex": "1.48.0",
|
|
60
61
|
"convex-test": "0.0.60",
|
|
61
|
-
"effect": "4.0.
|
|
62
|
+
"effect": "4.0.5",
|
|
62
63
|
"publint": "0.3.25",
|
|
63
64
|
"typescript": "6.0.3",
|
|
64
65
|
"vite-plus": "1.1.0",
|
|
65
66
|
"vitest": "5.0.3"
|
|
66
67
|
},
|
|
67
68
|
"peerDependencies": {
|
|
68
|
-
"@effect/platform-node": "^4.0.
|
|
69
|
-
"alchemy": "2.0.0-beta.
|
|
69
|
+
"@effect/platform-node": "^4.0.5",
|
|
70
|
+
"alchemy": "2.0.0-beta.84",
|
|
70
71
|
"convex": "^1.48.0",
|
|
71
|
-
"effect": "^4.0.
|
|
72
|
+
"effect": "^4.0.5"
|
|
72
73
|
},
|
|
73
74
|
"engines": {
|
|
74
75
|
"node": ">=22"
|
package/src/Component.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { toPath } from "alchemy/FQN";
|
|
2
|
+
import type { NamespaceNode } from "alchemy/Namespace";
|
|
3
|
+
import { Stack } from "alchemy/Stack";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import type { Deployment } from "./Deployment.ts";
|
|
7
|
+
|
|
8
|
+
/** A component's resources occupy this namespace in the deployment's Alchemy stack and stage. */
|
|
9
|
+
export interface ComponentBinding {
|
|
10
|
+
readonly type: "component";
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly namespace: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Associate a native namespace with an installed component. `component` is the
|
|
17
|
+
* installed name from the app's `convex.config.ts`; a nested install joins the
|
|
18
|
+
* names with slashes, such as `"workspace/mail"`. An undefined namespace
|
|
19
|
+
* associates the stack root. This records display metadata; it does not install
|
|
20
|
+
* the component, set its environment, or change removal policies.
|
|
21
|
+
*/
|
|
22
|
+
export const bindComponent = (options: {
|
|
23
|
+
deployment: Deployment;
|
|
24
|
+
component: string;
|
|
25
|
+
namespace: NamespaceNode | undefined;
|
|
26
|
+
}) =>
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
if (options.component === "") {
|
|
29
|
+
return yield* Effect.die(new Error("The component name is empty."));
|
|
30
|
+
}
|
|
31
|
+
const namespace = toPath(options.namespace);
|
|
32
|
+
const sid = `convex.component:${JSON.stringify(namespace)}`;
|
|
33
|
+
const stack = yield* Stack;
|
|
34
|
+
const previous = stack.bindings[options.deployment.FQN]?.find((binding) => binding.sid === sid);
|
|
35
|
+
// Alchemy keeps the last binding with a given sid. Reject conflicting owners before that happens.
|
|
36
|
+
if (previous !== undefined) {
|
|
37
|
+
const existing = Schema.decodeUnknownSync(Schema.Struct({ path: Schema.String }))(
|
|
38
|
+
previous.data,
|
|
39
|
+
);
|
|
40
|
+
if (existing.path !== options.component) {
|
|
41
|
+
return yield* Effect.die(
|
|
42
|
+
new Error(
|
|
43
|
+
`Alchemy namespace ${JSON.stringify(namespace)} is already bound to ${existing.path}.`,
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
yield* options.deployment.bind(sid, {
|
|
50
|
+
type: "component",
|
|
51
|
+
path: options.component,
|
|
52
|
+
namespace,
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/Deployment.ts
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
import { posix, win32 } from "node:path";
|
|
55
55
|
import { Resource } from "alchemy";
|
|
56
56
|
import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
|
|
57
|
-
import { isResolved } from "alchemy/Diff";
|
|
57
|
+
import { diffBindings, isResolved } from "alchemy/Diff";
|
|
58
58
|
import type { InputProps } from "alchemy/Input";
|
|
59
59
|
import * as Provider from "alchemy/Provider";
|
|
60
60
|
import * as Effect from "effect/Effect";
|
|
@@ -63,6 +63,7 @@ import * as Redacted from "effect/Redacted";
|
|
|
63
63
|
import * as Schema from "effect/Schema";
|
|
64
64
|
import { shouldAdopt } from "./Adoption.ts";
|
|
65
65
|
import { Credentials } from "./Credentials.ts";
|
|
66
|
+
import type { ComponentBinding } from "./Component.ts";
|
|
66
67
|
import {
|
|
67
68
|
absentAsUndefined,
|
|
68
69
|
type ConvexApiError,
|
|
@@ -145,7 +146,7 @@ export type Deployment = Resource<
|
|
|
145
146
|
"Convex.Deployment",
|
|
146
147
|
DeploymentProps,
|
|
147
148
|
DeploymentAttributes,
|
|
148
|
-
|
|
149
|
+
ComponentBinding,
|
|
149
150
|
Providers
|
|
150
151
|
>;
|
|
151
152
|
/** The resource class. Stacks call {@link Deployment}, which writes `rootDir` into the props. */
|
|
@@ -597,9 +598,10 @@ export const DeploymentProvider = () =>
|
|
|
597
598
|
// the refused props. Only `rootDir` compares with `olds`, because it
|
|
598
599
|
// is in the props only. A refused update leaves the row `updating`,
|
|
599
600
|
// and the next plan updates it again anyway.
|
|
600
|
-
diff: Effect.fn(function* ({ news, olds, output }) {
|
|
601
|
+
diff: Effect.fn(function* ({ news, olds, output, oldBindings, newBindings }) {
|
|
601
602
|
if (!isResolved(news) || output === undefined) return undefined;
|
|
602
603
|
yield* checkIdentity(output, news);
|
|
604
|
+
if (!isResolved(newBindings)) return undefined;
|
|
603
605
|
// State from 0.7 has no rootDir; the constructor always writes it.
|
|
604
606
|
const rootDir = news.rootDir ?? ".";
|
|
605
607
|
// Also here, so that a wrong rootDir stops the plan before Apply
|
|
@@ -617,9 +619,21 @@ export const DeploymentProvider = () =>
|
|
|
617
619
|
const current = yield* live(access, output);
|
|
618
620
|
if (current?.name !== output.name) return { action: "update" } as const;
|
|
619
621
|
const hash = yield* wantedHash(news, rootDir, output.name);
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
622
|
+
if (hash !== output.hash) return { action: "update" } as const;
|
|
623
|
+
if (diffBindings(oldBindings, newBindings).every((binding) => binding.action === "noop"))
|
|
624
|
+
return { action: "noop" } as const;
|
|
625
|
+
// Binding metadata does not change deployment outputs. Keep unchanged
|
|
626
|
+
// values available to consumers so the plan does not update them too.
|
|
627
|
+
return {
|
|
628
|
+
action: "update" as const,
|
|
629
|
+
stables: [
|
|
630
|
+
"hash",
|
|
631
|
+
"envNames",
|
|
632
|
+
...(
|
|
633
|
+
["projectId", "type", "previewName", "name", "reference", "url", "siteUrl"] as const
|
|
634
|
+
).filter((key) => current[key] === output[key]),
|
|
635
|
+
],
|
|
636
|
+
};
|
|
623
637
|
}),
|
|
624
638
|
|
|
625
639
|
read: Effect.fn(function* ({ olds, output }) {
|