@gaganref/confect-core 10.3.1-fork.8 → 10.3.1-fork.81
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/dist/ActionCall.d.ts +18 -0
- package/dist/ActionCall.d.ts.map +1 -0
- package/dist/ActionCall.js +21 -0
- package/dist/ActionCall.js.map +1 -0
- package/dist/App.d.ts +30 -93
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +24 -38
- package/dist/App.js.map +1 -1
- package/dist/Auth.d.ts +33 -9
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +17 -2
- package/dist/Auth.js.map +1 -1
- package/dist/Callback.d.ts +7 -2
- package/dist/Callback.d.ts.map +1 -1
- package/dist/Callback.js +11 -6
- package/dist/Callback.js.map +1 -1
- package/dist/Component.d.ts +98 -2
- package/dist/Component.d.ts.map +1 -1
- package/dist/Component.js +44 -18
- package/dist/Component.js.map +1 -1
- package/dist/FunctionSpec.d.ts +365 -2
- package/dist/FunctionSpec.d.ts.map +1 -1
- package/dist/FunctionSpec.js +66 -3
- package/dist/FunctionSpec.js.map +1 -1
- package/dist/GenericId.js +2 -2
- package/dist/GroupSpec.js +1 -1
- package/dist/Http.d.ts +4 -8
- package/dist/Http.d.ts.map +1 -1
- package/dist/Http.js +15 -22
- package/dist/Http.js.map +1 -1
- package/dist/ManifestSection.d.ts +70 -0
- package/dist/ManifestSection.d.ts.map +1 -0
- package/dist/ManifestSection.js +7 -0
- package/dist/ManifestSection.js.map +1 -0
- package/dist/Mount.d.ts +200 -0
- package/dist/Mount.d.ts.map +1 -0
- package/dist/Mount.js +116 -0
- package/dist/Mount.js.map +1 -0
- package/dist/PaginationError.js +1 -1
- package/dist/Profile.d.ts +95 -0
- package/dist/Profile.d.ts.map +1 -0
- package/dist/Profile.js +15 -0
- package/dist/Profile.js.map +1 -0
- package/dist/Ref.d.ts +4 -3
- package/dist/Ref.d.ts.map +1 -1
- package/dist/Ref.js +13 -19
- package/dist/Ref.js.map +1 -1
- package/dist/RefInternal.d.ts +3 -0
- package/dist/RefInternal.d.ts.map +1 -0
- package/dist/RefInternal.js +16 -0
- package/dist/RefInternal.js.map +1 -0
- package/dist/SchemaToValidator.d.ts +11 -2
- package/dist/SchemaToValidator.d.ts.map +1 -1
- package/dist/SchemaToValidator.js +43 -14
- package/dist/SchemaToValidator.js.map +1 -1
- package/dist/SystemFields.js +3 -3
- package/dist/Table.d.ts +48 -9
- package/dist/Table.d.ts.map +1 -1
- package/dist/Table.js +41 -13
- package/dist/Table.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/internal/annotations.d.ts +2 -0
- package/dist/internal/annotations.d.ts.map +1 -0
- package/dist/internal/annotations.js +6 -0
- package/dist/internal/annotations.js.map +1 -0
- package/dist/internal/scalarDocs.d.ts +5 -0
- package/dist/internal/scalarDocs.d.ts.map +1 -0
- package/dist/internal/scalarDocs.js +43 -0
- package/dist/internal/scalarDocs.js.map +1 -0
- package/package.json +3 -3
- package/src/ActionCall.ts +25 -0
- package/src/App.ts +36 -136
- package/src/Auth.ts +54 -11
- package/src/Callback.ts +17 -4
- package/src/Component.ts +196 -11
- package/src/FunctionSpec.ts +566 -5
- package/src/Http.ts +15 -37
- package/src/ManifestSection.ts +78 -0
- package/src/Mount.ts +401 -0
- package/src/Profile.ts +141 -0
- package/src/Ref.ts +19 -26
- package/src/RefInternal.ts +26 -0
- package/src/SchemaToValidator.ts +47 -5
- package/src/Table.ts +157 -23
- package/src/index.ts +4 -0
- package/src/internal/annotations.ts +1 -0
- package/src/internal/scalarDocs.ts +72 -0
package/src/Http.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as SchemaAST from "effect/SchemaAST";
|
|
|
7
7
|
import type * as HttpApi from "effect/http-api/HttpApi";
|
|
8
8
|
import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
|
|
9
9
|
import type * as HttpApiGroup from "effect/http-api/HttpApiGroup";
|
|
10
|
-
import
|
|
10
|
+
import { scalarDocs } from "./internal/scalarDocs.js";
|
|
11
11
|
import * as App from "./App.js";
|
|
12
12
|
|
|
13
13
|
const TypeId = "~@confect/core/Http";
|
|
@@ -57,10 +57,7 @@ export const make = <
|
|
|
57
57
|
const routes =
|
|
58
58
|
options.docs === undefined
|
|
59
59
|
? api
|
|
60
|
-
: Layer.merge(
|
|
61
|
-
api,
|
|
62
|
-
HttpApiScalar.layerCdn(options.api, { path: options.docs }),
|
|
63
|
-
);
|
|
60
|
+
: Layer.merge(api, scalarDocs(options.api, options.docs));
|
|
64
61
|
const http: Http<HttpApi.HttpApi<Id, Groups>, typeof routes> = {
|
|
65
62
|
[TypeId]: TypeId,
|
|
66
63
|
api: options.api,
|
|
@@ -73,10 +70,7 @@ export const make = <
|
|
|
73
70
|
export const isHttp = (u: unknown): u is Any =>
|
|
74
71
|
Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId;
|
|
75
72
|
|
|
76
|
-
export type Owner =
|
|
77
|
-
| { readonly kind: "app" }
|
|
78
|
-
| { readonly kind: "docs" }
|
|
79
|
-
| { readonly kind: "mount"; readonly mount: string };
|
|
73
|
+
export type Owner = { readonly kind: "app" } | { readonly kind: "docs" };
|
|
80
74
|
|
|
81
75
|
export interface Route {
|
|
82
76
|
readonly method: string;
|
|
@@ -104,7 +98,7 @@ export class DuplicateRoute extends Data.TaggedError("DuplicateRoute")<{
|
|
|
104
98
|
readonly second: Owner;
|
|
105
99
|
}> {
|
|
106
100
|
override get message(): string {
|
|
107
|
-
return `${this.method} ${this.path} is declared twice (${
|
|
101
|
+
return `${this.method} ${this.path} is declared twice (${this.first.kind} and ${this.second.kind}).`;
|
|
108
102
|
}
|
|
109
103
|
}
|
|
110
104
|
|
|
@@ -124,9 +118,6 @@ export type RouteError =
|
|
|
124
118
|
| DuplicateRoute
|
|
125
119
|
| OverlappingMountPrefixes;
|
|
126
120
|
|
|
127
|
-
const ownerLabel = (owner: Owner): string =>
|
|
128
|
-
owner.kind === "mount" ? `mount "${owner.mount}"` : owner.kind;
|
|
129
|
-
|
|
130
121
|
const trimSlash = (prefix: string): string =>
|
|
131
122
|
prefix.length > 1 && prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
132
123
|
|
|
@@ -160,14 +151,10 @@ interface KeyedRoute {
|
|
|
160
151
|
readonly key: string;
|
|
161
152
|
}
|
|
162
153
|
|
|
163
|
-
const apiRoutes = (
|
|
164
|
-
api: App.Api,
|
|
165
|
-
prefix: string,
|
|
166
|
-
owner: Owner,
|
|
167
|
-
): ReadonlyArray<KeyedRoute> =>
|
|
154
|
+
const apiRoutes = (api: App.Api, owner: Owner): ReadonlyArray<KeyedRoute> =>
|
|
168
155
|
Object.values<HttpApiGroup.Top>(api.groups).flatMap((group) =>
|
|
169
156
|
Object.values(group.endpoints).map((endpoint) => {
|
|
170
|
-
const path =
|
|
157
|
+
const path = endpoint.path;
|
|
171
158
|
const declared = paramNames(endpoint.params);
|
|
172
159
|
return {
|
|
173
160
|
route: { method: endpoint.method, path, owner },
|
|
@@ -177,20 +164,19 @@ const apiRoutes = (
|
|
|
177
164
|
);
|
|
178
165
|
|
|
179
166
|
/**
|
|
180
|
-
*
|
|
181
|
-
* mount's
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* when two mount prefixes overlap.
|
|
167
|
+
* The backend's own HTTP routes and docs page. Fails when a route is at or
|
|
168
|
+
* under a mount's prefix (Convex accepts that and lets the app route silently
|
|
169
|
+
* shadow the component's), when two routes share a method and path, or when two
|
|
170
|
+
* mount prefixes overlap.
|
|
185
171
|
*/
|
|
186
172
|
export const routes = (
|
|
187
173
|
app: App.Any,
|
|
188
174
|
http?: Any,
|
|
189
175
|
): Result.Result<ReadonlyArray<Route>, RouteError> => {
|
|
190
|
-
const mounts = App.mounts(app).flatMap(({ name, httpPrefix
|
|
176
|
+
const mounts = App.mounts(app).flatMap(({ name, httpPrefix }) =>
|
|
191
177
|
httpPrefix === undefined
|
|
192
178
|
? []
|
|
193
|
-
: [{ name, httpPrefix: trimSlash(httpPrefix)
|
|
179
|
+
: [{ name, httpPrefix: trimSlash(httpPrefix) }],
|
|
194
180
|
);
|
|
195
181
|
|
|
196
182
|
for (const [index, first] of mounts.entries()) {
|
|
@@ -213,7 +199,7 @@ export const routes = (
|
|
|
213
199
|
http === undefined
|
|
214
200
|
? []
|
|
215
201
|
: [
|
|
216
|
-
...apiRoutes(http.api,
|
|
202
|
+
...apiRoutes(http.api, { kind: "app" }),
|
|
217
203
|
...(http.docs === undefined
|
|
218
204
|
? []
|
|
219
205
|
: [
|
|
@@ -244,16 +230,8 @@ export const routes = (
|
|
|
244
230
|
}
|
|
245
231
|
}
|
|
246
232
|
|
|
247
|
-
const all = [
|
|
248
|
-
...own,
|
|
249
|
-
...mounts.flatMap(({ name, httpPrefix, api }) =>
|
|
250
|
-
api === undefined
|
|
251
|
-
? []
|
|
252
|
-
: apiRoutes(api, httpPrefix, { kind: "mount", mount: name }),
|
|
253
|
-
),
|
|
254
|
-
];
|
|
255
233
|
const seen = new Map<string, Route>();
|
|
256
|
-
for (const { route, key } of
|
|
234
|
+
for (const { route, key } of own) {
|
|
257
235
|
const previous = seen.get(key);
|
|
258
236
|
if (previous !== undefined) {
|
|
259
237
|
return Result.fail(
|
|
@@ -267,5 +245,5 @@ export const routes = (
|
|
|
267
245
|
}
|
|
268
246
|
seen.set(key, route);
|
|
269
247
|
}
|
|
270
|
-
return Result.succeed(
|
|
248
|
+
return Result.succeed(own.map(({ route }) => route));
|
|
271
249
|
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type * as FunctionSpec from "./FunctionSpec.js";
|
|
2
|
+
import type * as MiddlewareSpec from "./MiddlewareSpec.js";
|
|
3
|
+
|
|
4
|
+
export interface Data<Value = unknown> {
|
|
5
|
+
readonly format: number;
|
|
6
|
+
readonly functions: Readonly<Record<string, Value>>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What codegen hands a section's `render`: the exports of each `confect/` file
|
|
11
|
+
* it matched, every registered function with the spec file declaring it, each
|
|
12
|
+
* mounted component with the specifier of its definition, and every file
|
|
13
|
+
* `match` was offered.
|
|
14
|
+
*/
|
|
15
|
+
export interface GenerateInput {
|
|
16
|
+
readonly modules: ReadonlyArray<{
|
|
17
|
+
readonly relativePath: string;
|
|
18
|
+
readonly exports: Readonly<Record<string, unknown>>;
|
|
19
|
+
}>;
|
|
20
|
+
readonly functions: ReadonlyArray<{
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly source: string;
|
|
23
|
+
}>;
|
|
24
|
+
readonly mounts: ReadonlyArray<{
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly definition: string;
|
|
27
|
+
}>;
|
|
28
|
+
readonly files: ReadonlyArray<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Generate {
|
|
32
|
+
readonly file: string;
|
|
33
|
+
match(relativePath: string): boolean;
|
|
34
|
+
render(input: GenerateInput): {
|
|
35
|
+
readonly contents: string;
|
|
36
|
+
readonly errors: ReadonlyArray<string>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What codegen hands a section's `summarize`: the `confect/` files its
|
|
42
|
+
* `generate` matched, each function's description by path, and the spec file
|
|
43
|
+
* declaring each function by path, relative to `confect/`.
|
|
44
|
+
*/
|
|
45
|
+
export interface SummarizeInput<Value = unknown> {
|
|
46
|
+
readonly modules: GenerateInput["modules"];
|
|
47
|
+
readonly functions: Readonly<Record<string, Value>>;
|
|
48
|
+
readonly sources: Readonly<Record<string, string>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ManifestSection<
|
|
52
|
+
Value = unknown,
|
|
53
|
+
Data_ extends { readonly format: number } = Data<Value>,
|
|
54
|
+
> {
|
|
55
|
+
readonly owner: string;
|
|
56
|
+
readonly format: number;
|
|
57
|
+
describe(input: {
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly spec: FunctionSpec.AnyWithProps;
|
|
60
|
+
readonly middleware: ReadonlyArray<MiddlewareSpec.AnyMiddlewareSpec>;
|
|
61
|
+
}): Value;
|
|
62
|
+
summarize?(input: SummarizeInput<Value>): Data_;
|
|
63
|
+
check?(
|
|
64
|
+
next: Data_,
|
|
65
|
+
previous: unknown,
|
|
66
|
+
): {
|
|
67
|
+
readonly errors: ReadonlyArray<string>;
|
|
68
|
+
readonly notes: ReadonlyArray<string>;
|
|
69
|
+
};
|
|
70
|
+
readonly generate?: Generate;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type Any = ManifestSection<unknown, { readonly format: number }>;
|
|
74
|
+
|
|
75
|
+
export interface Entry {
|
|
76
|
+
readonly key: string;
|
|
77
|
+
readonly section: Any;
|
|
78
|
+
}
|
package/src/Mount.ts
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { componentsGeneric } from "convex/server";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Option from "effect/Option";
|
|
5
|
+
import * as Predicate from "effect/Predicate";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
8
|
+
import type * as App from "./App.js";
|
|
9
|
+
import * as Callback from "./Callback.js";
|
|
10
|
+
import * as Component from "./Component.js";
|
|
11
|
+
import type * as Env from "./Env.js";
|
|
12
|
+
import type * as GenericId from "./GenericId.js";
|
|
13
|
+
import type * as IdScope from "./IdScope.js";
|
|
14
|
+
import { CallbackContract } from "./internal/annotations.js";
|
|
15
|
+
import * as Ref from "./Ref.js";
|
|
16
|
+
|
|
17
|
+
const TypeId = "~@confect/core/Mount";
|
|
18
|
+
|
|
19
|
+
type Handle = Ref.AnyConfect & {
|
|
20
|
+
readonly "~@confect/core/Callback/Handle": Callback.Id;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ContractOf<H extends Handle> = Callback.Contract<
|
|
24
|
+
H["~@confect/core/Callback/Handle"],
|
|
25
|
+
Ref.GetFunctionType<H>,
|
|
26
|
+
H["args"]["fields"],
|
|
27
|
+
H["returns"],
|
|
28
|
+
Extract<NonNullable<H["error"]>, Schema.Top>
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What a mount passes for `init`'s args: the same shape, with a host ref in
|
|
33
|
+
* each callback slot.
|
|
34
|
+
*/
|
|
35
|
+
type Input<T> = T extends Handle
|
|
36
|
+
? Ref.AnyConfect
|
|
37
|
+
: T extends object
|
|
38
|
+
? { readonly [K in keyof T]: Input<T[K]> }
|
|
39
|
+
: T;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `A` when every alternative of `A` fits `T`; otherwise a type that refuses
|
|
43
|
+
* each alternative that does not. A callback slot takes an internal host ref
|
|
44
|
+
* that satisfies the slot's contract. Whether a slot may be left `undefined` is
|
|
45
|
+
* `Input`'s to say.
|
|
46
|
+
*/
|
|
47
|
+
type Check<T, A> = A extends unknown ? Fit<T, A> : never;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Distributes over the members of `T`, so a value is checked against the
|
|
51
|
+
* members whose shape it has and fits when it fits one of them.
|
|
52
|
+
*/
|
|
53
|
+
type Fit<T, A> = T extends Handle
|
|
54
|
+
? A extends Ref.AnyConfect
|
|
55
|
+
? A &
|
|
56
|
+
Callback.Check<ContractOf<T>, A> &
|
|
57
|
+
(Ref.GetFunctionVisibility<A> extends "internal"
|
|
58
|
+
? unknown
|
|
59
|
+
: { readonly callbackMustBeInternal: never })
|
|
60
|
+
: never
|
|
61
|
+
: T extends object
|
|
62
|
+
? A extends Input<T>
|
|
63
|
+
? {
|
|
64
|
+
readonly [K in keyof A]: K extends keyof T
|
|
65
|
+
? Check<T[K], A[K]>
|
|
66
|
+
: never;
|
|
67
|
+
}
|
|
68
|
+
: never
|
|
69
|
+
: A extends T
|
|
70
|
+
? A
|
|
71
|
+
: never;
|
|
72
|
+
|
|
73
|
+
type ArgsOf<C> = C extends { readonly init: infer I extends Component.Init }
|
|
74
|
+
? Ref.Args<I>
|
|
75
|
+
: never;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `[C]`, not a bare `C`, so the conditional is not distributive. A distributive
|
|
79
|
+
* one resolves against `Component.Descriptor` while `C` is inferred, drops `A`
|
|
80
|
+
* from the contextual type, and widens inline literals until `A` falls back to
|
|
81
|
+
* its constraint.
|
|
82
|
+
*/
|
|
83
|
+
type ArgsField<C, A> = [C] extends [
|
|
84
|
+
{ readonly init: infer I extends Component.Init },
|
|
85
|
+
]
|
|
86
|
+
? { readonly args: A & NoInfer<Check<Ref.Args<I>, A>> }
|
|
87
|
+
: { readonly args?: never };
|
|
88
|
+
|
|
89
|
+
type IsOptional<S> = S extends { readonly "~type.optionality": "optional" }
|
|
90
|
+
? true
|
|
91
|
+
: false;
|
|
92
|
+
|
|
93
|
+
type RequiredKeys<E> = {
|
|
94
|
+
[K in keyof E]: IsOptional<E[K]> extends true ? never : K;
|
|
95
|
+
}[keyof E];
|
|
96
|
+
|
|
97
|
+
type SlotValue<S> = Extract<Schema.Schema.Type<S & Schema.Top>, string>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What one env slot accepts: a constant, or an entry of the host's env whose
|
|
101
|
+
* value fits. A required slot takes only a required entry.
|
|
102
|
+
*/
|
|
103
|
+
type Slot<S> =
|
|
104
|
+
IsOptional<S> extends true
|
|
105
|
+
? SlotValue<S> | undefined | Env.Entry<string, any, SlotValue<S>, boolean>
|
|
106
|
+
: SlotValue<S> | Env.Entry<string, any, SlotValue<S>, false>;
|
|
107
|
+
|
|
108
|
+
type Bindings<E> = { readonly [K in RequiredKeys<E>]: Slot<E[K]> } & {
|
|
109
|
+
readonly [K in Exclude<keyof E, RequiredKeys<E>>]?: Slot<E[K]>;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `B` keeps the bindings as written, so `App.make` can see which backends their
|
|
114
|
+
* entries come from. `[C]` for the reason `ArgsField` gives: a bare `C` widens
|
|
115
|
+
* an inline constant to `string`.
|
|
116
|
+
*/
|
|
117
|
+
type EnvField<C, B> = [C] extends [{ readonly env: infer E }]
|
|
118
|
+
? [RequiredKeys<E>] extends [never]
|
|
119
|
+
? { readonly env?: B & Bindings<E> & NoExtra<B, E> }
|
|
120
|
+
: { readonly env: B & Bindings<E> & NoExtra<B, E> }
|
|
121
|
+
: { readonly env?: never };
|
|
122
|
+
|
|
123
|
+
type NoExtra<B, E> = { readonly [K in Exclude<keyof B, keyof E>]: never };
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The client constructor's parameters after the native mount decide whether
|
|
127
|
+
* `options` is absent, optional or required. A client that takes no native
|
|
128
|
+
* mount cannot be mounted. That branch is an object, not `never`, so the
|
|
129
|
+
* literal `name` and `args` beside it keep their types.
|
|
130
|
+
*/
|
|
131
|
+
type OptionsField<C extends Component.Descriptor> =
|
|
132
|
+
Parameters<C["client"]> extends [unknown, ...infer Rest]
|
|
133
|
+
? Rest extends []
|
|
134
|
+
? { readonly options?: never }
|
|
135
|
+
: Rest extends [infer O]
|
|
136
|
+
? { readonly options: O }
|
|
137
|
+
: Rest extends [options?: infer O]
|
|
138
|
+
? { readonly options?: Exclude<O, undefined> }
|
|
139
|
+
: never
|
|
140
|
+
: { readonly options: never };
|
|
141
|
+
|
|
142
|
+
export type Client<C extends Component.Descriptor> = ReturnType<C["client"]>;
|
|
143
|
+
|
|
144
|
+
export interface Data<C extends Component.Descriptor = Component.Descriptor> {
|
|
145
|
+
readonly component: C;
|
|
146
|
+
readonly name: string;
|
|
147
|
+
readonly httpPrefix?: App.HttpPrefix;
|
|
148
|
+
readonly env?: Readonly<Record<string, string | Env.AnyEntry | undefined>>;
|
|
149
|
+
readonly args?: unknown;
|
|
150
|
+
readonly options?: unknown;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* What a mount was declared with, as written: its HTTP prefix and env bindings.
|
|
155
|
+
*/
|
|
156
|
+
export interface Declared<
|
|
157
|
+
P extends App.HttpPrefix | undefined = App.HttpPrefix | undefined,
|
|
158
|
+
B = unknown,
|
|
159
|
+
> {
|
|
160
|
+
readonly httpPrefix: P;
|
|
161
|
+
readonly env: B;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A mounted Convex component. Yield it for its client. Its default is the
|
|
166
|
+
* component's client over this mount, and `Effect.provideService` replaces it.
|
|
167
|
+
*/
|
|
168
|
+
export interface Mount<
|
|
169
|
+
C extends Component.Descriptor,
|
|
170
|
+
Name extends string,
|
|
171
|
+
D extends Declared = Declared,
|
|
172
|
+
> extends Context.Reference<Client<C>> {
|
|
173
|
+
readonly [TypeId]: Data<C> & { readonly name: Name };
|
|
174
|
+
readonly "~declared"?: D;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type Any = Mount<Component.Descriptor, string>;
|
|
178
|
+
|
|
179
|
+
export type NameOf<M> = M extends Mount<any, infer N, any> ? N : never;
|
|
180
|
+
|
|
181
|
+
type InitArgs<C> = C extends { readonly init: infer I extends Component.Init }
|
|
182
|
+
? Ref.Args<I>
|
|
183
|
+
: never;
|
|
184
|
+
|
|
185
|
+
type CallbackSlot<C> = {
|
|
186
|
+
[K in keyof InitArgs<C>]-?: NonNullable<InitArgs<C>[K]> extends Handle
|
|
187
|
+
? K
|
|
188
|
+
: never;
|
|
189
|
+
}[keyof InitArgs<C>] &
|
|
190
|
+
string;
|
|
191
|
+
|
|
192
|
+
type SlotArgs<C, K extends PropertyKey> = Extract<
|
|
193
|
+
NonNullable<InitArgs<C>[K & keyof InitArgs<C>]>,
|
|
194
|
+
Handle
|
|
195
|
+
>["args"]["fields"];
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `callbackArgs`, on a mount whose descriptor carries its contract.
|
|
199
|
+
*/
|
|
200
|
+
type CallbackArgs<C, Name extends string> = C extends {
|
|
201
|
+
readonly contract: Component.Component<any, infer From, any>;
|
|
202
|
+
}
|
|
203
|
+
? {
|
|
204
|
+
/**
|
|
205
|
+
* A callback slot's contract args, with the component's ids typed for
|
|
206
|
+
* this mount, for the host function that answers the callback.
|
|
207
|
+
*/
|
|
208
|
+
readonly callbackArgs: <const K extends CallbackSlot<C>>(
|
|
209
|
+
slot: K,
|
|
210
|
+
) => GenericId.RebaseSchema<
|
|
211
|
+
Schema.Struct<SlotArgs<C, K>>,
|
|
212
|
+
From,
|
|
213
|
+
Component.MountScope<IdScope.App, Name>
|
|
214
|
+
>["fields"];
|
|
215
|
+
}
|
|
216
|
+
: unknown;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Mount a Convex component, as Convex's `app.use` does, in the host's
|
|
220
|
+
* `confect/<mount>.ts`. `args` is typed from the component's `init`, `env` from
|
|
221
|
+
* its env slots, and `options` from its client constructor. A handler reaches
|
|
222
|
+
* the client with `yield* BulkEmail`.
|
|
223
|
+
*
|
|
224
|
+
* ```ts
|
|
225
|
+
* export const BulkEmail = Mount.make(Email, {
|
|
226
|
+
* name: "bulkEmail",
|
|
227
|
+
* httpPrefix: "/bulk-email",
|
|
228
|
+
* env: { RESEND_API_KEY: env.BULK_RESEND_KEY },
|
|
229
|
+
* args: {
|
|
230
|
+
* fromName: "School office",
|
|
231
|
+
* onEvent: refs.internal.circulars.onEmailEvent,
|
|
232
|
+
* },
|
|
233
|
+
* });
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export const make = <
|
|
237
|
+
C extends Component.Descriptor,
|
|
238
|
+
const Name extends string,
|
|
239
|
+
const A extends Input<ArgsOf<C>>,
|
|
240
|
+
const P extends App.HttpPrefix = never,
|
|
241
|
+
const B = {},
|
|
242
|
+
>(
|
|
243
|
+
component: C,
|
|
244
|
+
options: {
|
|
245
|
+
readonly name: Name;
|
|
246
|
+
readonly httpPrefix?: P;
|
|
247
|
+
} & EnvField<C, B> &
|
|
248
|
+
ArgsField<C, A> &
|
|
249
|
+
OptionsField<C>,
|
|
250
|
+
): Mount<C, Name, Declared<[P] extends [never] ? undefined : P, B>> &
|
|
251
|
+
CallbackArgs<C, Name> => {
|
|
252
|
+
const data = { ...(options as object), component } as Data<C> & {
|
|
253
|
+
readonly name: Name;
|
|
254
|
+
};
|
|
255
|
+
// The options were checked against the constructor's parameters above.
|
|
256
|
+
const client = component.client as unknown as (
|
|
257
|
+
native: unknown,
|
|
258
|
+
options?: unknown,
|
|
259
|
+
) => Client<C>;
|
|
260
|
+
const native = () => componentsGeneric()[data.name];
|
|
261
|
+
const mount = Context.Reference<Client<C>>(`mount/${data.name}`, {
|
|
262
|
+
defaultValue: () => client(native(), data.options),
|
|
263
|
+
});
|
|
264
|
+
const { contract, init } = component;
|
|
265
|
+
const callbackArgs = (slot: string) => {
|
|
266
|
+
const field = init!.args.fields[slot]!;
|
|
267
|
+
const callback = SchemaAST.resolveAt<Callback.Any>(CallbackContract)(
|
|
268
|
+
Schema.toEncoded(field).ast,
|
|
269
|
+
)!;
|
|
270
|
+
const bound = Component.bind(contract!, native() as never);
|
|
271
|
+
return Component.schema(bound, callback.functionProvenance.args).fields;
|
|
272
|
+
};
|
|
273
|
+
return Object.assign(
|
|
274
|
+
mount,
|
|
275
|
+
{ [TypeId]: data },
|
|
276
|
+
contract === undefined ? {} : { callbackArgs },
|
|
277
|
+
) as Mount<C, Name, Declared<[P] extends [never] ? undefined : P, B>> &
|
|
278
|
+
CallbackArgs<C, Name>;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* What a mount was declared with: its component, name, HTTP prefix, env, args
|
|
283
|
+
* and options.
|
|
284
|
+
*/
|
|
285
|
+
export const data = <C extends Component.Descriptor, Name extends string>(
|
|
286
|
+
mount: Mount<C, Name, Declared>,
|
|
287
|
+
): Data<C> & { readonly name: Name } => mount[TypeId];
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* What a push runs for a mount: its component's `init`, at the mount's
|
|
291
|
+
* component path, with the install args encoded.
|
|
292
|
+
*/
|
|
293
|
+
export interface InitCall {
|
|
294
|
+
readonly componentPath: string;
|
|
295
|
+
readonly functionName: string;
|
|
296
|
+
readonly args: unknown;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const contractOf = (schema: Schema.Top) =>
|
|
300
|
+
SchemaAST.resolveAt<Callback.Any>(CallbackContract)(
|
|
301
|
+
Schema.toEncoded(schema).ast,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
type Create<E, R> = (
|
|
305
|
+
contract: Callback.Any,
|
|
306
|
+
ref: Ref.Any,
|
|
307
|
+
) => Effect.Effect<unknown, E, R>;
|
|
308
|
+
|
|
309
|
+
const placeholderHandle: Create<never, never> = (contract) =>
|
|
310
|
+
Schema.decodeEffect(Callback.handle(contract))("").pipe(Effect.orDie);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handles for the union member `value` takes: the first member that fits once
|
|
314
|
+
* its callback slots hold placeholder handles and whose handles `create` makes.
|
|
315
|
+
* A target that satisfies only a later member's contract takes that member.
|
|
316
|
+
*/
|
|
317
|
+
const withMemberHandles = <E, R>(
|
|
318
|
+
members: ReadonlyArray<Schema.Top>,
|
|
319
|
+
value: unknown,
|
|
320
|
+
create: Create<E, R>,
|
|
321
|
+
): Effect.Effect<unknown, E, R> => {
|
|
322
|
+
const fitting = members.filter((member) =>
|
|
323
|
+
Schema.is(member)(
|
|
324
|
+
Effect.runSync(withHandles(member, value, placeholderHandle)),
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
return fitting.length === 0
|
|
328
|
+
? Effect.succeed(value)
|
|
329
|
+
: Effect.firstSuccessOf(
|
|
330
|
+
fitting.map((member) => withHandles(member, value, create)),
|
|
331
|
+
);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The schema of each key's value in a struct or a record.
|
|
336
|
+
*/
|
|
337
|
+
const entrySchema = (
|
|
338
|
+
schema: Schema.Top,
|
|
339
|
+
): ((key: string) => Schema.Top | undefined) | undefined => {
|
|
340
|
+
if (Predicate.hasProperty(schema, "fields")) {
|
|
341
|
+
const fields = schema.fields as Readonly<Record<string, Schema.Top>>;
|
|
342
|
+
return (key) => fields[key];
|
|
343
|
+
}
|
|
344
|
+
if (SchemaAST.isObjects(schema.ast) && Predicate.hasProperty(schema, "value"))
|
|
345
|
+
return () => schema.value as Schema.Top;
|
|
346
|
+
return undefined;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Put a handle in each callback slot: a host ref in a field whose schema
|
|
351
|
+
* carries a callback contract, at any depth of struct fields and record values,
|
|
352
|
+
* through `optionalKey`, `optional` and the union member the value takes.
|
|
353
|
+
*/
|
|
354
|
+
const withHandles = <E, R>(
|
|
355
|
+
schema: Schema.Top,
|
|
356
|
+
value: unknown,
|
|
357
|
+
create: Create<E, R>,
|
|
358
|
+
): Effect.Effect<unknown, E, R> => {
|
|
359
|
+
const contract = contractOf(schema);
|
|
360
|
+
if (contract !== undefined && Ref.isRef(value))
|
|
361
|
+
return create(contract, value);
|
|
362
|
+
if (Predicate.hasProperty(schema, "schema"))
|
|
363
|
+
return withHandles(schema.schema as Schema.Top, value, create);
|
|
364
|
+
if (Predicate.hasProperty(schema, "members") && Array.isArray(schema.members))
|
|
365
|
+
return withMemberHandles(schema.members, value, create);
|
|
366
|
+
const at = entrySchema(schema);
|
|
367
|
+
if (at === undefined || !Predicate.isObject(value))
|
|
368
|
+
return Effect.succeed(value);
|
|
369
|
+
return Effect.forEach(Object.entries(value), ([key, field]) => {
|
|
370
|
+
const fieldSchema = at(key);
|
|
371
|
+
return fieldSchema === undefined
|
|
372
|
+
? Effect.succeed([key, field] as const)
|
|
373
|
+
: withHandles(fieldSchema, field, create).pipe(
|
|
374
|
+
Effect.map((resolved) => [key, resolved] as const),
|
|
375
|
+
);
|
|
376
|
+
}).pipe(Effect.map(Object.fromEntries));
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The `init` call a mount's install args make, with each callback slot's host
|
|
381
|
+
* ref replaced by the handle `create` makes for the slot's contract. None for a
|
|
382
|
+
* component without `init`.
|
|
383
|
+
*/
|
|
384
|
+
export const initCall = <E, R>(
|
|
385
|
+
mount: Any,
|
|
386
|
+
create: Create<E, R>,
|
|
387
|
+
): Effect.Effect<Option.Option<InitCall>, E | Schema.SchemaError, R> => {
|
|
388
|
+
const { component, name, args } = data(mount);
|
|
389
|
+
const init = component.init;
|
|
390
|
+
if (init === undefined) return Effect.succeedNone;
|
|
391
|
+
return withHandles(init.args, args ?? {}, create).pipe(
|
|
392
|
+
Effect.flatMap(Schema.encodeUnknownEffect(init.args)),
|
|
393
|
+
Effect.map((encoded) =>
|
|
394
|
+
Option.some({
|
|
395
|
+
componentPath: name,
|
|
396
|
+
functionName: init.convexFunctionName,
|
|
397
|
+
args: encoded,
|
|
398
|
+
}),
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
};
|