@confect/server 7.0.0 → 9.0.0-next.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/CHANGELOG.md +70 -0
- package/dist/DatabaseSchema.d.ts +6 -6
- package/dist/DatabaseSchema.d.ts.map +1 -1
- package/dist/DatabaseSchema.js +3 -3
- package/dist/DatabaseSchema.js.map +1 -1
- package/dist/Document.d.ts.map +1 -1
- package/dist/Document.js +35 -23
- package/dist/Document.js.map +1 -1
- package/dist/FunctionImpl.d.ts +10 -7
- package/dist/FunctionImpl.d.ts.map +1 -1
- package/dist/FunctionImpl.js +8 -8
- package/dist/FunctionImpl.js.map +1 -1
- package/dist/GroupImpl.d.ts +51 -12
- package/dist/GroupImpl.d.ts.map +1 -1
- package/dist/GroupImpl.js +72 -4
- package/dist/GroupImpl.js.map +1 -1
- package/dist/GroupPath.d.ts +8 -0
- package/dist/GroupPath.d.ts.map +1 -0
- package/dist/GroupPath.js +10 -0
- package/dist/GroupPath.js.map +1 -0
- package/dist/RegisteredConvexFunction.d.ts +6 -6
- package/dist/RegisteredConvexFunction.d.ts.map +1 -1
- package/dist/RegisteredConvexFunction.js +18 -7
- package/dist/RegisteredConvexFunction.js.map +1 -1
- package/dist/RegisteredFunction.d.ts +3 -3
- package/dist/RegisteredFunction.d.ts.map +1 -1
- package/dist/RegisteredFunctions.d.ts +15 -4
- package/dist/RegisteredFunctions.d.ts.map +1 -1
- package/dist/RegisteredFunctions.js +20 -11
- package/dist/RegisteredFunctions.js.map +1 -1
- package/dist/SchemaToValidator.d.ts +8 -8
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/package.json +21 -19
- package/src/DatabaseSchema.ts +5 -5
- package/src/Document.ts +90 -58
- package/src/FunctionImpl.ts +27 -36
- package/src/GroupImpl.ts +168 -32
- package/src/GroupPath.ts +43 -0
- package/src/RegisteredConvexFunction.ts +18 -17
- package/src/RegisteredFunctions.ts +78 -28
- package/src/index.ts +0 -2
- package/dist/Impl.d.ts +0 -24
- package/dist/Impl.d.ts.map +0 -1
- package/dist/Impl.js +0 -28
- package/dist/Impl.js.map +0 -1
- package/dist/Registry.d.ts +0 -15
- package/dist/Registry.d.ts.map +0 -1
- package/dist/Registry.js +0 -10
- package/dist/Registry.js.map +0 -1
- package/src/Impl.ts +0 -59
- package/src/Registry.ts +0 -13
package/src/DatabaseSchema.ts
CHANGED
|
@@ -9,7 +9,11 @@ import * as Table from "./Table";
|
|
|
9
9
|
export const TypeId = "@confect/server/DatabaseSchema";
|
|
10
10
|
export type TypeId = typeof TypeId;
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export interface Any {
|
|
13
|
+
readonly [TypeId]: TypeId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const isDatabaseSchema = (u: unknown): u is Any =>
|
|
13
17
|
Predicate.hasProperty(u, TypeId);
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -31,10 +35,6 @@ export interface DatabaseSchema<Tables_ extends Table.AnyWithProps = never> {
|
|
|
31
35
|
): DatabaseSchema<Tables_ | TableDef>;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
export interface Any {
|
|
35
|
-
readonly [TypeId]: TypeId;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
38
|
export interface AnyWithProps {
|
|
39
39
|
readonly [TypeId]: TypeId;
|
|
40
40
|
readonly tables: Record<string, Table.AnyWithProps>;
|
package/src/Document.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import * as SystemFields from "@confect/core/SystemFields";
|
|
1
2
|
import { Effect, Function, ParseResult, pipe, Schema } from "effect";
|
|
2
3
|
import type { ReadonlyRecord } from "effect/Record";
|
|
3
|
-
import * as SystemFields from "@confect/core/SystemFields";
|
|
4
4
|
import type * as DataModel from "./DataModel";
|
|
5
5
|
import type { ReadonlyValue } from "./SchemaToValidator";
|
|
6
6
|
import type * as TableInfo from "./TableInfo";
|
|
@@ -10,6 +10,37 @@ export type WithoutSystemFields<Doc> = Omit<Doc, "_creationTime" | "_id">;
|
|
|
10
10
|
export type Any = any;
|
|
11
11
|
export type AnyEncoded = ReadonlyRecord<string, ReadonlyValue>;
|
|
12
12
|
|
|
13
|
+
type Decode = (doc: unknown) => Effect.Effect<unknown, ParseResult.ParseError>;
|
|
14
|
+
|
|
15
|
+
const decoderCache = new WeakMap<
|
|
16
|
+
Schema.Schema.AnyNoContext,
|
|
17
|
+
Map<string, Decode>
|
|
18
|
+
>();
|
|
19
|
+
|
|
20
|
+
const getDecoder = (
|
|
21
|
+
tableName: string,
|
|
22
|
+
tableSchema: Schema.Schema.AnyNoContext,
|
|
23
|
+
): Decode => {
|
|
24
|
+
const byTable =
|
|
25
|
+
decoderCache.get(tableSchema) ??
|
|
26
|
+
(() => {
|
|
27
|
+
const map = new Map<string, Decode>();
|
|
28
|
+
decoderCache.set(tableSchema, map);
|
|
29
|
+
return map;
|
|
30
|
+
})();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
byTable.get(tableName) ??
|
|
34
|
+
(() => {
|
|
35
|
+
const decoder = Schema.decode(
|
|
36
|
+
SystemFields.extendWithSystemFields(tableName, tableSchema),
|
|
37
|
+
) as Decode;
|
|
38
|
+
byTable.set(tableName, decoder);
|
|
39
|
+
return decoder;
|
|
40
|
+
})()
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
13
44
|
export const decode = Function.dual<
|
|
14
45
|
<
|
|
15
46
|
DataModel_ extends DataModel.AnyWithProps,
|
|
@@ -53,36 +84,43 @@ export const decode = Function.dual<
|
|
|
53
84
|
DataModel.TableInfoWithName_<DataModel_, TableName>["document"],
|
|
54
85
|
DocumentDecodeError
|
|
55
86
|
> =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
),
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return decodedDoc;
|
|
83
|
-
}),
|
|
87
|
+
pipe(
|
|
88
|
+
self,
|
|
89
|
+
getDecoder(tableName, tableSchema),
|
|
90
|
+
Effect.catchIf(ParseResult.isParseError, (parseError) =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
const formattedParseError =
|
|
93
|
+
yield* ParseResult.TreeFormatter.formatError(parseError);
|
|
94
|
+
|
|
95
|
+
return yield* new DocumentDecodeError({
|
|
96
|
+
tableName,
|
|
97
|
+
id: self._id,
|
|
98
|
+
parseError: formattedParseError,
|
|
99
|
+
});
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
Effect.map(
|
|
103
|
+
(decodedDoc) =>
|
|
104
|
+
decodedDoc as DataModel.TableInfoWithName_<
|
|
105
|
+
DataModel_,
|
|
106
|
+
TableName
|
|
107
|
+
>["document"],
|
|
108
|
+
),
|
|
109
|
+
),
|
|
84
110
|
);
|
|
85
111
|
|
|
112
|
+
type Encode = (doc: unknown) => Effect.Effect<unknown, ParseResult.ParseError>;
|
|
113
|
+
|
|
114
|
+
const encoderCache = new WeakMap<Schema.Schema.AnyNoContext, Encode>();
|
|
115
|
+
|
|
116
|
+
const getEncoder = (tableSchema: Schema.Schema.AnyNoContext): Encode =>
|
|
117
|
+
encoderCache.get(tableSchema) ??
|
|
118
|
+
(() => {
|
|
119
|
+
const encoder = Schema.encode(tableSchema) as Encode;
|
|
120
|
+
encoderCache.set(tableSchema, encoder);
|
|
121
|
+
return encoder;
|
|
122
|
+
})();
|
|
123
|
+
|
|
86
124
|
export const encode = Function.dual<
|
|
87
125
|
<
|
|
88
126
|
DataModel_ extends DataModel.AnyWithProps,
|
|
@@ -126,35 +164,29 @@ export const encode = Function.dual<
|
|
|
126
164
|
DataModel.TableInfoWithName_<DataModel_, TableName>["encodedDocument"],
|
|
127
165
|
DocumentEncodeError
|
|
128
166
|
> =>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}),
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
return encodedDoc;
|
|
157
|
-
}),
|
|
167
|
+
pipe(
|
|
168
|
+
self,
|
|
169
|
+
getEncoder(tableSchema),
|
|
170
|
+
Effect.catchIf(ParseResult.isParseError, (parseError) =>
|
|
171
|
+
Effect.gen(function* () {
|
|
172
|
+
const formattedParseError =
|
|
173
|
+
yield* ParseResult.TreeFormatter.formatError(parseError);
|
|
174
|
+
|
|
175
|
+
return yield* new DocumentEncodeError({
|
|
176
|
+
tableName,
|
|
177
|
+
id: self._id,
|
|
178
|
+
parseError: formattedParseError,
|
|
179
|
+
});
|
|
180
|
+
}),
|
|
181
|
+
),
|
|
182
|
+
Effect.map(
|
|
183
|
+
(encodedDoc) =>
|
|
184
|
+
encodedDoc as DataModel.TableInfoWithName_<
|
|
185
|
+
DataModel_,
|
|
186
|
+
TableName
|
|
187
|
+
>["encodedDocument"],
|
|
188
|
+
),
|
|
189
|
+
),
|
|
158
190
|
);
|
|
159
191
|
|
|
160
192
|
export class DocumentDecodeError extends Schema.TaggedError<DocumentDecodeError>()(
|
package/src/FunctionImpl.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type * as FunctionSpec from "@confect/core/FunctionSpec";
|
|
2
|
-
import type * as GroupPath from "@confect/core/GroupPath";
|
|
3
2
|
import type * as GroupSpec from "@confect/core/GroupSpec";
|
|
4
|
-
import
|
|
3
|
+
import * as Registry from "@confect/core/Registry";
|
|
4
|
+
import { Context, Effect, Layer, Ref, String } from "effect";
|
|
5
5
|
import type * as Api from "./Api";
|
|
6
|
+
import { resolveGroupPathUnsafe } from "./GroupPath";
|
|
6
7
|
import type * as Handler from "./Handler";
|
|
7
8
|
import { setNestedProperty } from "./internal/utils";
|
|
8
|
-
import * as Registry from "./Registry";
|
|
9
9
|
import * as RegistryItem from "./RegistryItem";
|
|
10
10
|
|
|
11
11
|
export interface FunctionImpl<
|
|
@@ -32,34 +32,23 @@ export const FunctionImpl = <
|
|
|
32
32
|
|
|
33
33
|
export const make = <
|
|
34
34
|
Api_ extends Api.AnyWithProps,
|
|
35
|
-
|
|
36
|
-
const FunctionName extends FunctionSpec.Name<
|
|
37
|
-
GroupSpec.Functions<GroupPath.GroupAt<Api.Groups<Api_>, GroupPath_>>
|
|
38
|
-
>,
|
|
35
|
+
Group extends GroupSpec.AnyWithProps,
|
|
36
|
+
const FunctionName extends FunctionSpec.Name<GroupSpec.Functions<Group>>,
|
|
39
37
|
>(
|
|
40
38
|
api: Api_,
|
|
41
|
-
|
|
39
|
+
group: Group,
|
|
42
40
|
functionName: FunctionName,
|
|
43
41
|
handler: Handler.WithName<
|
|
44
42
|
Api.Schema<Api_>,
|
|
45
|
-
GroupSpec.Functions<
|
|
43
|
+
GroupSpec.Functions<Group>,
|
|
46
44
|
FunctionName
|
|
47
45
|
>,
|
|
48
|
-
): Layer.Layer<FunctionImpl<
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
const group_: GroupSpec.AnyWithProps = Array.reduce(
|
|
53
|
-
restGroupPathParts,
|
|
54
|
-
(api as any).spec.groups[firstGroupPathPart as any]!,
|
|
55
|
-
(currentGroup: any, groupPathPart: any) =>
|
|
56
|
-
currentGroup.groups[groupPathPart],
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const functionSpec = group_.functions[functionName]!;
|
|
46
|
+
): Layer.Layer<FunctionImpl<string, FunctionName>> => {
|
|
47
|
+
const groupPath = resolveGroupPathUnsafe(api.spec, group);
|
|
48
|
+
const functionSpec = group.functions[functionName]!;
|
|
60
49
|
|
|
61
50
|
return Layer.effect(
|
|
62
|
-
FunctionImpl<
|
|
51
|
+
FunctionImpl<string, FunctionName>({
|
|
63
52
|
groupPath,
|
|
64
53
|
functionName,
|
|
65
54
|
}),
|
|
@@ -69,7 +58,7 @@ export const make = <
|
|
|
69
58
|
yield* Ref.update(registry, (registryItems) =>
|
|
70
59
|
setNestedProperty(
|
|
71
60
|
registryItems,
|
|
72
|
-
[...
|
|
61
|
+
[...String.split(groupPath, "."), functionName],
|
|
73
62
|
RegistryItem.make({
|
|
74
63
|
functionSpec,
|
|
75
64
|
handler,
|
|
@@ -94,19 +83,21 @@ export type ForGroupPathAndFunction<
|
|
|
94
83
|
> = FunctionImpl<GroupPath_, FunctionName>;
|
|
95
84
|
|
|
96
85
|
/**
|
|
97
|
-
* Get all function implementation services required for a group
|
|
86
|
+
* Get all function implementation services required for a group spec.
|
|
98
87
|
*/
|
|
99
|
-
export type
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
? FunctionSpec.Name<
|
|
106
|
-
GroupSpec.Functions<GroupAtPath>
|
|
107
|
-
> extends infer FunctionNames extends string
|
|
108
|
-
? FunctionNames extends string
|
|
109
|
-
? FunctionImpl<GroupPath_, FunctionNames>
|
|
110
|
-
: never
|
|
88
|
+
export type FromGroupSpec<Group extends GroupSpec.AnyWithProps> =
|
|
89
|
+
FunctionSpec.Name<
|
|
90
|
+
GroupSpec.Functions<Group>
|
|
91
|
+
> extends infer FunctionNames extends string
|
|
92
|
+
? FunctionNames extends string
|
|
93
|
+
? FunctionImpl<string, FunctionNames>
|
|
111
94
|
: never
|
|
112
95
|
: never;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @deprecated Use {@link FromGroupSpec} instead.
|
|
99
|
+
*/
|
|
100
|
+
export type FromGroupAtPath<
|
|
101
|
+
_GroupPath extends string,
|
|
102
|
+
Group extends GroupSpec.AnyWithProps,
|
|
103
|
+
> = FromGroupSpec<Group>;
|
package/src/GroupImpl.ts
CHANGED
|
@@ -1,60 +1,196 @@
|
|
|
1
|
-
import type * as GroupPath from "@confect/core/GroupPath";
|
|
2
1
|
import type * as GroupSpec from "@confect/core/GroupSpec";
|
|
3
|
-
import
|
|
2
|
+
import * as Registry from "@confect/core/Registry";
|
|
3
|
+
import {
|
|
4
|
+
Array,
|
|
5
|
+
Context,
|
|
6
|
+
Effect,
|
|
7
|
+
Layer,
|
|
8
|
+
Option,
|
|
9
|
+
pipe,
|
|
10
|
+
Predicate,
|
|
11
|
+
Record,
|
|
12
|
+
Ref,
|
|
13
|
+
String,
|
|
14
|
+
} from "effect";
|
|
4
15
|
import type * as Api from "./Api";
|
|
5
16
|
import type * as FunctionImpl from "./FunctionImpl";
|
|
17
|
+
import { resolveGroupPathUnsafe } from "./GroupPath";
|
|
6
18
|
|
|
7
|
-
export
|
|
19
|
+
export const TypeId = "@confect/server/GroupImpl";
|
|
20
|
+
export type TypeId = typeof TypeId;
|
|
21
|
+
|
|
22
|
+
export type FinalizationStatus = "Unfinalized" | "Finalized";
|
|
23
|
+
|
|
24
|
+
export interface GroupImpl<
|
|
25
|
+
GroupPath_ extends string,
|
|
26
|
+
FinalizationStatus_ extends FinalizationStatus = "Unfinalized",
|
|
27
|
+
> {
|
|
28
|
+
readonly [TypeId]: TypeId;
|
|
8
29
|
readonly groupPath: GroupPath_;
|
|
30
|
+
readonly finalizationStatus: FinalizationStatus_;
|
|
31
|
+
/**
|
|
32
|
+
* Names of every function registered into this group's layer scope by
|
|
33
|
+
* `FunctionImpl.make`. Authoritative only when `finalizationStatus` is
|
|
34
|
+
* `"Finalized"`; the `"Unfinalized"` value is set to `[]` at `make`-time
|
|
35
|
+
* since the list is only known once `finalize` snapshots the registry.
|
|
36
|
+
*/
|
|
37
|
+
readonly registeredFunctionNames: ReadonlyArray<string>;
|
|
9
38
|
}
|
|
10
39
|
|
|
11
|
-
export
|
|
40
|
+
export interface Any extends GroupImpl<string, FinalizationStatus> {}
|
|
41
|
+
|
|
42
|
+
export const isGroupImpl = (u: unknown): u is Any =>
|
|
43
|
+
Predicate.hasProperty(u, TypeId);
|
|
44
|
+
|
|
45
|
+
export interface AnyFinalized extends GroupImpl<string, "Finalized"> {}
|
|
46
|
+
export interface AnyUnfinalized extends GroupImpl<string, "Unfinalized"> {}
|
|
47
|
+
|
|
48
|
+
export const isFinalizedGroupImpl = (u: unknown): u is AnyFinalized =>
|
|
49
|
+
isGroupImpl(u) && u.finalizationStatus === "Finalized";
|
|
50
|
+
|
|
51
|
+
export const isUnfinalizedGroupImpl = (u: unknown): u is AnyUnfinalized =>
|
|
52
|
+
isGroupImpl(u) && u.finalizationStatus === "Unfinalized";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build the runtime tag for a `GroupImpl` service. The finalization status is
|
|
56
|
+
* embedded in the tag string so that `Unfinalized` and `Finalized` are distinct
|
|
57
|
+
* services at runtime; consumers of a finalized layer (the server's
|
|
58
|
+
* `RegisteredFunctions.buildForGroup` and the CLI's `implValidation`) retrieve
|
|
59
|
+
* the typed `Finalized` service directly rather than scanning the context.
|
|
60
|
+
*/
|
|
61
|
+
export const GroupImpl = <
|
|
62
|
+
GroupPath_ extends string,
|
|
63
|
+
FinalizationStatus_ extends FinalizationStatus,
|
|
64
|
+
>({
|
|
12
65
|
groupPath,
|
|
66
|
+
finalizationStatus,
|
|
13
67
|
}: {
|
|
14
68
|
groupPath: GroupPath_;
|
|
69
|
+
finalizationStatus: FinalizationStatus_;
|
|
15
70
|
}) =>
|
|
16
|
-
Context.GenericTag<GroupImpl<GroupPath_>>(
|
|
17
|
-
`@confect/server/GroupImpl/${groupPath}`,
|
|
71
|
+
Context.GenericTag<GroupImpl<GroupPath_, FinalizationStatus_>>(
|
|
72
|
+
`@confect/server/GroupImpl/${finalizationStatus}/${groupPath}`,
|
|
18
73
|
);
|
|
19
74
|
|
|
20
75
|
export const make = <
|
|
21
76
|
Api_ extends Api.AnyWithProps,
|
|
22
|
-
|
|
77
|
+
Group extends GroupSpec.AnyWithProps,
|
|
23
78
|
>(
|
|
24
|
-
|
|
25
|
-
|
|
79
|
+
api: Api_,
|
|
80
|
+
group: Group,
|
|
26
81
|
): Layer.Layer<
|
|
27
|
-
GroupImpl<
|
|
82
|
+
GroupImpl<string, "Unfinalized">,
|
|
28
83
|
never,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
84
|
+
FunctionImpl.FromGroupSpec<Group>
|
|
85
|
+
> => {
|
|
86
|
+
const groupPath = resolveGroupPathUnsafe(api.spec, group);
|
|
87
|
+
|
|
88
|
+
return Layer.succeed(
|
|
89
|
+
GroupImpl<string, "Unfinalized">({
|
|
34
90
|
groupPath,
|
|
91
|
+
finalizationStatus: "Unfinalized",
|
|
35
92
|
}),
|
|
36
93
|
{
|
|
94
|
+
[TypeId]: TypeId,
|
|
37
95
|
groupPath,
|
|
96
|
+
finalizationStatus: "Unfinalized" as const,
|
|
97
|
+
registeredFunctionNames: [],
|
|
38
98
|
},
|
|
39
99
|
) as Layer.Layer<
|
|
40
|
-
GroupImpl<
|
|
100
|
+
GroupImpl<string, "Unfinalized">,
|
|
41
101
|
never,
|
|
42
|
-
|
|
43
|
-
| FunctionImpl.FromGroupAtPath<GroupPath_, Api.Groups<Api_>>
|
|
102
|
+
FunctionImpl.FromGroupSpec<Group>
|
|
44
103
|
>;
|
|
104
|
+
};
|
|
45
105
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
: Groups extends GroupSpec.AnyWithProps
|
|
49
|
-
? GroupImpl<GroupSpec.Name<Groups>>
|
|
50
|
-
: never;
|
|
106
|
+
const isFunctionShaped = (value: unknown): boolean =>
|
|
107
|
+
Predicate.isRecord(value) && "functionSpec" in value;
|
|
51
108
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Walk a `RegistryItems` tree to the entries at `groupPath` and return the
|
|
111
|
+
* names of the function-shaped leaves directly underneath.
|
|
112
|
+
*/
|
|
113
|
+
const collectFunctionNamesAtPath = (
|
|
114
|
+
items: Registry.RegistryItems,
|
|
115
|
+
groupPath: string,
|
|
116
|
+
): ReadonlyArray<string> =>
|
|
117
|
+
pipe(
|
|
118
|
+
String.split(groupPath, "."),
|
|
119
|
+
Array.reduce(Option.some<unknown>(items), (acc, segment) =>
|
|
120
|
+
acc.pipe(
|
|
121
|
+
Option.filter(Predicate.isRecord),
|
|
122
|
+
Option.flatMap((node) =>
|
|
123
|
+
segment in node ? Option.some(node[segment]) : Option.none(),
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
Option.filter(Predicate.isRecord),
|
|
128
|
+
Option.map(Record.toEntries),
|
|
129
|
+
Option.map(
|
|
130
|
+
Array.filterMap(([name, value]) =>
|
|
131
|
+
isFunctionShaped(value) ? Option.some(name) : Option.none(),
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
Option.getOrElse((): ReadonlyArray<string> => []),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const findUnfinalizedGroupImpl = <S>(
|
|
138
|
+
context: Context.Context<S>,
|
|
139
|
+
): Option.Option<AnyUnfinalized> =>
|
|
140
|
+
Array.findFirst(context.unsafeMap.values(), isUnfinalizedGroupImpl);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Mark a `GroupImpl` layer as fully implemented. The parameter type defaults
|
|
144
|
+
* `RIn = never`, so passing a layer that still requires any `FunctionImpl`
|
|
145
|
+
* service produces a type error at the impl author's site. The codegen
|
|
146
|
+
* boundary requires the resulting `"Finalized"` brand, so omitting this call
|
|
147
|
+
* is also rejected downstream.
|
|
148
|
+
*
|
|
149
|
+
* As a side effect of finalization, the names of every `FunctionImpl` that
|
|
150
|
+
* registered into this group's scope are snapshotted onto the produced
|
|
151
|
+
* service value's `registeredFunctionNames` field, so consumers can verify
|
|
152
|
+
* impl completeness against a `GroupSpec`'s expected functions without
|
|
153
|
+
* having to inspect the `Registry` themselves.
|
|
154
|
+
*/
|
|
155
|
+
export const finalize = <GroupPath_ extends string>(
|
|
156
|
+
group: Layer.Layer<GroupImpl<GroupPath_, "Unfinalized">>,
|
|
157
|
+
): Layer.Layer<GroupImpl<GroupPath_, "Finalized">> =>
|
|
158
|
+
Layer.flatMap(
|
|
159
|
+
group,
|
|
160
|
+
(context): Layer.Layer<GroupImpl<GroupPath_, "Finalized">> =>
|
|
161
|
+
findUnfinalizedGroupImpl(context).pipe(
|
|
162
|
+
Option.match({
|
|
163
|
+
onNone: () =>
|
|
164
|
+
Layer.die(
|
|
165
|
+
new Error(
|
|
166
|
+
"GroupImpl.finalize: no Unfinalized GroupImpl service was found in the layer's context.",
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
onSome: (unfinalized) => {
|
|
170
|
+
const groupPath = unfinalized.groupPath as GroupPath_;
|
|
171
|
+
return Layer.effect(
|
|
172
|
+
GroupImpl<GroupPath_, "Finalized">({
|
|
173
|
+
groupPath,
|
|
174
|
+
finalizationStatus: "Finalized",
|
|
175
|
+
}),
|
|
176
|
+
Effect.gen(function* () {
|
|
177
|
+
const registry = yield* Registry.Registry;
|
|
178
|
+
const items = yield* Ref.get(registry);
|
|
179
|
+
return {
|
|
180
|
+
[TypeId]: TypeId,
|
|
181
|
+
groupPath,
|
|
182
|
+
finalizationStatus: "Finalized" as const,
|
|
183
|
+
registeredFunctionNames: collectFunctionNamesAtPath(
|
|
184
|
+
items,
|
|
185
|
+
groupPath,
|
|
186
|
+
),
|
|
187
|
+
};
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
export type FromGroupSpec<Group extends GroupSpec.AnyWithProps> =
|
|
196
|
+
FunctionImpl.FromGroupSpec<Group>;
|
package/src/GroupPath.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type * as GroupSpec from "@confect/core/GroupSpec";
|
|
2
|
+
import type * as Spec from "@confect/core/Spec";
|
|
3
|
+
import { Array, Option, pipe, Record } from "effect";
|
|
4
|
+
|
|
5
|
+
const resolveGroupPathInGroup = (
|
|
6
|
+
group: GroupSpec.AnyWithProps,
|
|
7
|
+
target: GroupSpec.AnyWithProps,
|
|
8
|
+
pathSegments: ReadonlyArray<string>,
|
|
9
|
+
): Option.Option<string> =>
|
|
10
|
+
pipe(
|
|
11
|
+
Record.toEntries(group.groups),
|
|
12
|
+
Array.findFirst(([name, child]) =>
|
|
13
|
+
child === target
|
|
14
|
+
? Option.some(Array.join([...pathSegments, name], "."))
|
|
15
|
+
: resolveGroupPathInGroup(child, target, [...pathSegments, name]),
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const resolveGroupPath = (
|
|
20
|
+
spec: Spec.AnyWithProps,
|
|
21
|
+
target: GroupSpec.AnyWithProps,
|
|
22
|
+
): Option.Option<string> =>
|
|
23
|
+
pipe(
|
|
24
|
+
Record.toEntries(spec.groups),
|
|
25
|
+
Array.findFirst(([name, group]) =>
|
|
26
|
+
group === target
|
|
27
|
+
? Option.some(name)
|
|
28
|
+
: resolveGroupPathInGroup(group, target, [name]),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const resolveGroupPathUnsafe = (
|
|
33
|
+
spec: Spec.AnyWithProps,
|
|
34
|
+
target: GroupSpec.AnyWithProps,
|
|
35
|
+
): string =>
|
|
36
|
+
resolveGroupPath(spec, target).pipe(
|
|
37
|
+
Option.getOrThrowWith(
|
|
38
|
+
() =>
|
|
39
|
+
new Error(
|
|
40
|
+
"Could not resolve group path for the provided GroupSpec. Ensure the spec is part of the assembled API spec tree.",
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
@@ -98,27 +98,28 @@ export const make = <Api_ extends Api.AnyWithPropsWithRuntime<"Convex">>(
|
|
|
98
98
|
Match.exhaustive,
|
|
99
99
|
);
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Convex's query cache is invalidated by any Date.now() call during handler
|
|
103
|
+
* execution. Effect's unsafeFork calls Date.now() when constructing a
|
|
104
|
+
* FiberId.Runtime, which trips the cache for every confect-wrapped query. We
|
|
105
|
+
* stub Date.now to 0 for the span of the handler; queries are forbidden from
|
|
106
|
+
* relying on real time for correctness anyway.
|
|
107
|
+
*
|
|
108
|
+
* Users who explicitly want the real timestamp can still reach it via Effect's
|
|
109
|
+
* Clock service (Clock.currentTimeMillis/Clock.currentTimeNanos). We provide a
|
|
110
|
+
* Clock whose user-facing Effects call realDateNow (Convex's tracker) directly,
|
|
111
|
+
* making Clock an explicit opt-in to cache invalidation. The unsafe methods
|
|
112
|
+
* used internally by Effect (logging, span events, scheduler) return constants
|
|
113
|
+
* so they never touch the tracker—caching is not broken by default.
|
|
114
|
+
*/
|
|
111
115
|
const unpatchedClock = (realDateNow: () => number): Clock.Clock => {
|
|
112
|
-
const bigint1e6 = BigInt(1_000_000);
|
|
113
|
-
const unsafeCurrentTimeMillis = () => realDateNow();
|
|
114
|
-
const unsafeCurrentTimeNanos = () => BigInt(realDateNow()) * bigint1e6;
|
|
115
116
|
const defaultClock = Clock.make();
|
|
116
117
|
return {
|
|
117
118
|
...defaultClock,
|
|
118
|
-
unsafeCurrentTimeMillis,
|
|
119
|
-
unsafeCurrentTimeNanos,
|
|
120
|
-
currentTimeMillis: Effect.sync(
|
|
121
|
-
currentTimeNanos: Effect.sync(
|
|
119
|
+
unsafeCurrentTimeMillis: () => 0,
|
|
120
|
+
unsafeCurrentTimeNanos: () => 0n,
|
|
121
|
+
currentTimeMillis: Effect.sync(() => realDateNow()),
|
|
122
|
+
currentTimeNanos: Effect.sync(() => BigInt(realDateNow()) * 1_000_000n),
|
|
122
123
|
};
|
|
123
124
|
};
|
|
124
125
|
|