@gaganref/confect-server 10.3.1-fork.9 → 10.3.1-fork.90
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 +30 -0
- package/dist/ActionCall.d.ts.map +1 -0
- package/dist/ActionCall.js +41 -0
- package/dist/ActionCall.js.map +1 -0
- package/dist/ActionRunner.d.ts +1 -1
- package/dist/ActionRunner.d.ts.map +1 -1
- package/dist/ActionRunner.js +3 -3
- package/dist/ActionRunner.js.map +1 -1
- package/dist/AiGatewayDecisionClient.js +1 -1
- package/dist/AiGatewayLanguageClient.js +1 -1
- package/dist/Auth.d.ts +31 -2
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +23 -3
- package/dist/Auth.js.map +1 -1
- package/dist/Callback.d.ts +1 -1
- package/dist/Callback.d.ts.map +1 -1
- package/dist/Callback.js +6 -6
- package/dist/Callback.js.map +1 -1
- package/dist/ComponentCtx.d.ts +4 -0
- package/dist/ComponentCtx.d.ts.map +1 -1
- package/dist/ConvexConfigProvider.js +2 -2
- package/dist/ConvexFunctionDefinitions.d.ts.map +1 -1
- package/dist/ConvexFunctionDefinitions.js +4 -4
- package/dist/ConvexFunctionDefinitions.js.map +1 -1
- package/dist/ConvexLogger.js +1 -1
- package/dist/CronJobs.d.ts.map +1 -1
- package/dist/CronJobs.js +7 -6
- package/dist/CronJobs.js.map +1 -1
- package/dist/DatabaseSchema.js +2 -2
- package/dist/DatabaseWriter.d.ts +44 -2
- package/dist/DatabaseWriter.d.ts.map +1 -1
- package/dist/DatabaseWriter.js +59 -9
- package/dist/DatabaseWriter.js.map +1 -1
- package/dist/ExecutionMetadata.js +1 -1
- package/dist/FunctionImpl.js +2 -2
- package/dist/FunctionRegistryItem.d.ts +1 -0
- package/dist/FunctionRegistryItem.d.ts.map +1 -1
- package/dist/FunctionRegistryItem.js +2 -1
- package/dist/FunctionRegistryItem.js.map +1 -1
- package/dist/GroupImpl.d.ts +21 -2
- package/dist/GroupImpl.d.ts.map +1 -1
- package/dist/GroupImpl.js +21 -22
- package/dist/GroupImpl.js.map +1 -1
- package/dist/GroupRegistry.d.ts +1 -0
- package/dist/GroupRegistry.d.ts.map +1 -1
- package/dist/GroupRegistry.js +10 -3
- package/dist/GroupRegistry.js.map +1 -1
- package/dist/Handler.d.ts +8 -4
- package/dist/Handler.d.ts.map +1 -1
- package/dist/Handler.js.map +1 -1
- package/dist/HttpRouter.d.ts +3 -0
- package/dist/HttpRouter.d.ts.map +1 -1
- package/dist/HttpRouter.js +29 -10
- package/dist/HttpRouter.js.map +1 -1
- package/dist/MiddlewareImpl.js +1 -1
- package/dist/MountInit.d.ts +31 -0
- package/dist/MountInit.d.ts.map +1 -0
- package/dist/MountInit.js +49 -0
- package/dist/MountInit.js.map +1 -0
- package/dist/MutationRunner.d.ts +1 -1
- package/dist/MutationRunner.d.ts.map +1 -1
- package/dist/MutationRunner.js +3 -3
- package/dist/MutationRunner.js.map +1 -1
- package/dist/MutationTransactionContext.d.ts.map +1 -1
- package/dist/MutationTransactionContext.js +4 -3
- package/dist/MutationTransactionContext.js.map +1 -1
- package/dist/OfficialComponent.d.ts +60 -0
- package/dist/OfficialComponent.d.ts.map +1 -0
- package/dist/OfficialComponent.js +72 -0
- package/dist/OfficialComponent.js.map +1 -0
- package/dist/OrderedQuery.d.ts +4 -5
- package/dist/OrderedQuery.d.ts.map +1 -1
- package/dist/OrderedQuery.js.map +1 -1
- package/dist/QueryInitializer.js +2 -2
- package/dist/QueryRunner.js +2 -2
- package/dist/QueryStream.d.ts +120 -12
- package/dist/QueryStream.d.ts.map +1 -1
- package/dist/QueryStream.js +143 -41
- package/dist/QueryStream.js.map +1 -1
- package/dist/QueryStreamCursor.js +1 -1
- package/dist/QueryStreamIndexPrefix.js +1 -1
- package/dist/QueryStreamIndexRange.js +2 -2
- package/dist/QueryStreamKey.js +1 -1
- package/dist/QueryStreamKeyBounds.js +2 -2
- package/dist/QueryStreamKeyLayout.js +2 -2
- package/dist/QueryStreamKeyValues.js +2 -2
- package/dist/QueryStreamPagination.js +1 -1
- package/dist/QueryStreamReadBudget.d.ts +3 -2
- package/dist/QueryStreamReadBudget.d.ts.map +1 -1
- package/dist/QueryStreamReadBudget.js +8 -5
- package/dist/QueryStreamReadBudget.js.map +1 -1
- package/dist/QueryTransactionContext.js +2 -2
- package/dist/RegisteredConvexFunction.d.ts +10 -5
- package/dist/RegisteredConvexFunction.d.ts.map +1 -1
- package/dist/RegisteredConvexFunction.js +49 -29
- package/dist/RegisteredConvexFunction.js.map +1 -1
- package/dist/RegisteredFunction.d.ts +48 -22
- package/dist/RegisteredFunction.d.ts.map +1 -1
- package/dist/RegisteredFunction.js +163 -42
- package/dist/RegisteredFunction.js.map +1 -1
- package/dist/RegisteredNodeFunction.d.ts +1 -1
- package/dist/RegisteredNodeFunction.d.ts.map +1 -1
- package/dist/RegisteredNodeFunction.js +10 -6
- package/dist/RegisteredNodeFunction.js.map +1 -1
- package/dist/RegistryItems.d.ts +7 -0
- package/dist/RegistryItems.d.ts.map +1 -1
- package/dist/RegistryItems.js +8 -3
- package/dist/RegistryItems.js.map +1 -1
- package/dist/RequestMetadata.js +1 -1
- package/dist/Scheduler.d.ts +1 -1
- package/dist/Scheduler.d.ts.map +1 -1
- package/dist/Scheduler.js +14 -13
- package/dist/Scheduler.js.map +1 -1
- package/dist/Storage.js +1 -1
- package/dist/StorageActionWriter.js +1 -1
- package/dist/StorageReader.js +1 -1
- package/dist/StorageWriter.js +1 -1
- package/dist/Table.d.ts +6 -6
- package/dist/Table.d.ts.map +1 -1
- package/dist/TableInfo.d.ts +7 -7
- package/dist/TableInfo.d.ts.map +1 -1
- package/dist/TableInfo.js.map +1 -1
- package/dist/TransactionMetadata.js +1 -1
- package/dist/VectorSearch.js +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -18
- package/dist/internal/AiGatewayDecisionClient.js +1 -1
- package/dist/internal/AiGatewayLanguageClient.js +1 -1
- package/dist/internal/AiGatewayServiceToken.js +1 -1
- package/dist/internal/DocumentPatch.d.ts +16 -0
- package/dist/internal/DocumentPatch.d.ts.map +1 -0
- package/dist/internal/DocumentPatch.js +36 -0
- package/dist/internal/DocumentPatch.js.map +1 -0
- package/dist/internal/httpBody.d.ts +13 -0
- package/dist/internal/httpBody.d.ts.map +1 -0
- package/dist/internal/httpBody.js +47 -0
- package/dist/internal/httpBody.js.map +1 -0
- package/dist/internal/httpOptions.d.ts +4 -0
- package/dist/internal/httpOptions.d.ts.map +1 -0
- package/dist/internal/httpOptions.js +83 -0
- package/dist/internal/httpOptions.js.map +1 -0
- package/dist/internal/observability.d.ts +5 -0
- package/dist/internal/observability.d.ts.map +1 -0
- package/dist/internal/observability.js +21 -0
- package/dist/internal/observability.js.map +1 -0
- package/dist/internal/reservedArgument.d.ts +8 -0
- package/dist/internal/reservedArgument.d.ts.map +1 -0
- package/dist/internal/reservedArgument.js +30 -0
- package/dist/internal/reservedArgument.js.map +1 -0
- package/package.json +5 -5
- package/src/ActionCall.ts +83 -0
- package/src/ActionRunner.ts +3 -2
- package/src/Auth.ts +49 -1
- package/src/Callback.ts +3 -2
- package/src/ComponentCtx.ts +2 -0
- package/src/ConvexFunctionDefinitions.ts +2 -1
- package/src/CronJobs.ts +2 -1
- package/src/DatabaseWriter.ts +190 -26
- package/src/FunctionRegistryItem.ts +4 -0
- package/src/GroupImpl.ts +61 -8
- package/src/GroupRegistry.ts +7 -3
- package/src/Handler.ts +21 -5
- package/src/HttpRouter.ts +69 -5
- package/src/MountInit.ts +80 -0
- package/src/MutationRunner.ts +3 -2
- package/src/MutationTransactionContext.ts +2 -1
- package/src/OfficialComponent.ts +168 -0
- package/src/OrderedQuery.ts +4 -5
- package/src/QueryStream.ts +391 -61
- package/src/QueryStreamReadBudget.ts +19 -6
- package/src/RegisteredConvexFunction.ts +39 -2
- package/src/RegisteredFunction.ts +305 -49
- package/src/RegisteredNodeFunction.ts +11 -1
- package/src/RegistryItems.ts +31 -0
- package/src/Scheduler.ts +17 -19
- package/src/TableInfo.ts +18 -22
- package/src/index.ts +3 -0
- package/src/internal/DocumentPatch.ts +87 -0
- package/src/internal/httpBody.ts +72 -0
- package/src/internal/httpOptions.ts +154 -0
- package/src/internal/observability.ts +27 -0
- package/src/internal/reservedArgument.ts +56 -0
package/src/RegistryItems.ts
CHANGED
|
@@ -38,3 +38,34 @@ export const middlewareKeys = (items: RegistryItems): ReadonlyArray<string> =>
|
|
|
38
38
|
Array.filter(MiddlewareRegistryItem.isMiddlewareRegistryItem),
|
|
39
39
|
Array.map((item) => item.middlewareSpec.key),
|
|
40
40
|
);
|
|
41
|
+
|
|
42
|
+
export interface MissingMiddleware {
|
|
43
|
+
readonly functionName: string;
|
|
44
|
+
readonly middlewareKey: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const missingRequired = (
|
|
48
|
+
items: RegistryItems,
|
|
49
|
+
required: ReadonlyArray<{ readonly key: string }>,
|
|
50
|
+
groupPath = "",
|
|
51
|
+
): ReadonlyArray<MissingMiddleware> =>
|
|
52
|
+
Object.entries(items)
|
|
53
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
54
|
+
.flatMap(([functionName, value]) =>
|
|
55
|
+
FunctionRegistryItem.isFunctionRegistryItem(value)
|
|
56
|
+
? required.flatMap(({ key }) =>
|
|
57
|
+
value._tag === "Confect" &&
|
|
58
|
+
value.middlewareAttachments.some(({ spec }) => spec.key === key)
|
|
59
|
+
? []
|
|
60
|
+
: [
|
|
61
|
+
{
|
|
62
|
+
functionName:
|
|
63
|
+
groupPath === ""
|
|
64
|
+
? functionName
|
|
65
|
+
: `${groupPath}.${functionName}`,
|
|
66
|
+
middlewareKey: key,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
: [],
|
|
71
|
+
);
|
package/src/Scheduler.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import * as ReservedArgument from "./internal/reservedArgument.js";
|
|
1
2
|
import type * as IdScope from "@confect/core/IdScope";
|
|
2
|
-
import { Ref } from "@confect/core";
|
|
3
|
+
import type { Ref } from "@confect/core";
|
|
4
|
+
import { getFunctionReference } from "@confect/core/RefInternal";
|
|
3
5
|
import type { GenericId } from "@confect/core/GenericId";
|
|
4
6
|
import type { Scheduler as ConvexScheduler } from "convex/server";
|
|
5
7
|
import type { GenericId as ConvexGenericId } from "convex/values";
|
|
@@ -12,38 +14,34 @@ import * as Layer from "effect/Layer";
|
|
|
12
14
|
const make = (scheduler: ConvexScheduler) => ({
|
|
13
15
|
cancel: (id: ConvexGenericId<"_scheduled_functions">) =>
|
|
14
16
|
Effect.promise(() => scheduler.cancel(id)),
|
|
15
|
-
runAfter:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
...args: Ref.OptionalArgs<Ref_>
|
|
19
|
-
) => {
|
|
17
|
+
runAfter: Effect.fn("Scheduler.runAfter")(function* <
|
|
18
|
+
Ref_ extends Ref.AnyMutation | Ref.AnyAction,
|
|
19
|
+
>(delay: Duration.Duration, ref: Ref_, ...args: Ref.OptionalArgs<Ref_>) {
|
|
20
20
|
const delayMs = Duration.toMillis(delay);
|
|
21
|
-
const functionReference =
|
|
22
|
-
const encodedArgs =
|
|
21
|
+
const functionReference = getFunctionReference(ref);
|
|
22
|
+
const encodedArgs = yield* ReservedArgument.encodeArgs(
|
|
23
23
|
ref,
|
|
24
24
|
(args[0] ?? {}) as Ref.Args<Ref_>,
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
-
return Effect.promise(() =>
|
|
27
|
+
return yield* Effect.promise(() =>
|
|
28
28
|
scheduler.runAfter(delayMs, functionReference, encodedArgs),
|
|
29
29
|
);
|
|
30
|
-
},
|
|
31
|
-
runAt:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
...args: Ref.OptionalArgs<Ref_>
|
|
35
|
-
) => {
|
|
30
|
+
}),
|
|
31
|
+
runAt: Effect.fn("Scheduler.runAt")(function* <
|
|
32
|
+
Ref_ extends Ref.AnyMutation | Ref.AnyAction,
|
|
33
|
+
>(dateTime: DateTime.DateTime, ref: Ref_, ...args: Ref.OptionalArgs<Ref_>) {
|
|
36
34
|
const timestamp = DateTime.toEpochMillis(dateTime);
|
|
37
|
-
const functionReference =
|
|
38
|
-
const encodedArgs =
|
|
35
|
+
const functionReference = getFunctionReference(ref);
|
|
36
|
+
const encodedArgs = yield* ReservedArgument.encodeArgs(
|
|
39
37
|
ref,
|
|
40
38
|
(args[0] ?? {}) as Ref.Args<Ref_>,
|
|
41
39
|
);
|
|
42
40
|
|
|
43
|
-
return Effect.promise(() =>
|
|
41
|
+
return yield* Effect.promise(() =>
|
|
44
42
|
scheduler.runAt(timestamp, functionReference, encodedArgs),
|
|
45
43
|
);
|
|
46
|
-
},
|
|
44
|
+
}),
|
|
47
45
|
});
|
|
48
46
|
|
|
49
47
|
export const Scheduler = Context.Service<ReturnType<typeof make>>(
|
package/src/TableInfo.ts
CHANGED
|
@@ -18,39 +18,35 @@ import type * as Table from "./Table.js";
|
|
|
18
18
|
export declare const TypeId: "~@confect/server/TableInfo";
|
|
19
19
|
export type TypeId = typeof TypeId;
|
|
20
20
|
|
|
21
|
+
// Read through Table's extractors, not `infer`: an intersection with a Table,
|
|
22
|
+
// or an interface extending one, matches `Table<infer ...>` only structurally,
|
|
23
|
+
// which widens the name and scope. `Indexes` stays inferred: a generic table's
|
|
24
|
+
// index names resolve only through the inferred type's constraint.
|
|
21
25
|
export type TableInfo<Table_ extends Table.AnyWithProps> =
|
|
22
|
-
Table_ extends Table.Table<
|
|
23
|
-
infer TableName,
|
|
24
|
-
infer TableSchema_,
|
|
25
|
-
infer TableValidator,
|
|
26
|
-
infer Indexes,
|
|
27
|
-
infer SearchIndexes,
|
|
28
|
-
infer VectorIndexes,
|
|
29
|
-
infer Scope
|
|
30
|
-
>
|
|
26
|
+
Table_ extends Table.Table<any, any, any, infer Indexes, any, any, any, any>
|
|
31
27
|
? {
|
|
32
28
|
readonly [TypeId]: TypeId;
|
|
33
29
|
readonly document: WithSystemFields<
|
|
34
|
-
|
|
35
|
-
Schema.Schema.Type<
|
|
36
|
-
Scope
|
|
30
|
+
Table.Name<Table_>,
|
|
31
|
+
Schema.Schema.Type<Table.TableSchema<Table_>>,
|
|
32
|
+
Table.Scope<Table_>
|
|
37
33
|
>;
|
|
38
34
|
readonly encodedDocument: WithSystemFields<
|
|
39
|
-
|
|
40
|
-
Schema.Codec.Encoded<
|
|
41
|
-
Scope
|
|
35
|
+
Table.Name<Table_>,
|
|
36
|
+
Schema.Codec.Encoded<Table.TableSchema<Table_>>,
|
|
37
|
+
Table.Scope<Table_>
|
|
42
38
|
>;
|
|
43
39
|
readonly convexDocument: ExtractConvexDocument<
|
|
44
|
-
|
|
45
|
-
TableValidator
|
|
46
|
-
Scope
|
|
40
|
+
Table.Name<Table_>,
|
|
41
|
+
Table.TableValidator<Table_>,
|
|
42
|
+
Table.Scope<Table_>
|
|
47
43
|
>;
|
|
48
44
|
readonly fieldPaths:
|
|
49
|
-
| keyof IdField<
|
|
50
|
-
| ExtractFieldPaths<TableValidator
|
|
45
|
+
| keyof IdField<Table.Name<Table_>>
|
|
46
|
+
| ExtractFieldPaths<Table.TableValidator<Table_>>;
|
|
51
47
|
readonly indexes: Types.Simplify<Indexes & SystemIndexes>;
|
|
52
|
-
readonly searchIndexes: SearchIndexes
|
|
53
|
-
readonly vectorIndexes: VectorIndexes
|
|
48
|
+
readonly searchIndexes: Table.SearchIndexes<Table_>;
|
|
49
|
+
readonly vectorIndexes: Table.VectorIndexes<Table_>;
|
|
54
50
|
}
|
|
55
51
|
: never;
|
|
56
52
|
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * as ActionCall from "./ActionCall.js";
|
|
1
2
|
export * as AiGatewayDecisionClient from "./AiGatewayDecisionClient.js";
|
|
2
3
|
export * as AiGatewayDecisionModel from "./AiGatewayDecisionModel.js";
|
|
3
4
|
export * as AiGatewayLanguageClient from "./AiGatewayLanguageClient.js";
|
|
@@ -25,9 +26,11 @@ export * as Handler from "./Handler.js";
|
|
|
25
26
|
export * as HttpRouter from "./HttpRouter.js";
|
|
26
27
|
export * as MiddlewareImpl from "./MiddlewareImpl.js";
|
|
27
28
|
export * as MiddlewareRegistryItem from "./MiddlewareRegistryItem.js";
|
|
29
|
+
export * as MountInit from "./MountInit.js";
|
|
28
30
|
export * as MutationCtx from "./MutationCtx.js";
|
|
29
31
|
export * as MutationRunner from "./MutationRunner.js";
|
|
30
32
|
export * as MutationTransactionContext from "./MutationTransactionContext.js";
|
|
33
|
+
export * as OfficialComponent from "./OfficialComponent.js";
|
|
31
34
|
export * as OrderedQuery from "./OrderedQuery.js";
|
|
32
35
|
export * as QueryCtx from "./QueryCtx.js";
|
|
33
36
|
export * as QueryInitializer from "./QueryInitializer.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ReadonlyValue } from "@confect/core/SchemaToValidator";
|
|
2
|
+
import { pipe } from "effect/Function";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Record from "effect/Record";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
7
|
+
import type * as Table from "../Table.js";
|
|
8
|
+
|
|
9
|
+
export type EncodedPatch = Record.ReadonlyRecord<
|
|
10
|
+
string,
|
|
11
|
+
ReadonlyValue | undefined
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export type PatchEncoder = (
|
|
15
|
+
patch: Record.ReadonlyRecord<string, unknown>,
|
|
16
|
+
) => Effect.Effect<EncodedPatch, Schema.SchemaError>;
|
|
17
|
+
|
|
18
|
+
const patchEncoders = new WeakMap<Table.AnyWithProps, PatchEncoder | null>();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the encoder for patches to the table's documents, or `undefined` when
|
|
22
|
+
* the table's codec does not encode each field independently. The encoder maps
|
|
23
|
+
* every patched field of the table to its encoded value, or to `undefined` when
|
|
24
|
+
* the patch unsets the field or the field's codec encodes the value as an
|
|
25
|
+
* absent key. Keys outside the table's fields are left out.
|
|
26
|
+
*/
|
|
27
|
+
export const encoder = (
|
|
28
|
+
table: Table.AnyWithProps,
|
|
29
|
+
): PatchEncoder | undefined => {
|
|
30
|
+
let cached = patchEncoders.get(table);
|
|
31
|
+
if (cached === undefined) {
|
|
32
|
+
cached = makePatchEncoder(table);
|
|
33
|
+
patchEncoders.set(table, cached);
|
|
34
|
+
}
|
|
35
|
+
return cached ?? undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// A struct with no encoding of its own encodes each field through that field's
|
|
39
|
+
// codec alone, so a patch encodes field by field. A union or a transformation
|
|
40
|
+
// over the whole struct can make one field's encoding depend on another field.
|
|
41
|
+
// A struct-level check must see the decoded fields before they are encoded,
|
|
42
|
+
// and keys under an index signature have no field codec of their own.
|
|
43
|
+
const makePatchEncoder = (table: Table.AnyWithProps): PatchEncoder | null => {
|
|
44
|
+
const ast = table.Fields.ast;
|
|
45
|
+
if (
|
|
46
|
+
!SchemaAST.isObjects(ast) ||
|
|
47
|
+
ast.encoding !== undefined ||
|
|
48
|
+
ast.checks !== undefined ||
|
|
49
|
+
ast.indexSignatures.length > 0
|
|
50
|
+
) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// A field's codec runs only when the patch gives the field a value, since a
|
|
54
|
+
// codec can handle a missing key. Each comes from the table, whose codecs
|
|
55
|
+
// need no services.
|
|
56
|
+
const fieldEncoders: Record.ReadonlyRecord<
|
|
57
|
+
string,
|
|
58
|
+
(
|
|
59
|
+
value: unknown,
|
|
60
|
+
) => Effect.Effect<ReadonlyValue | undefined, Schema.SchemaError>
|
|
61
|
+
> = Object.fromEntries(
|
|
62
|
+
ast.propertySignatures.map(({ name, type }) => {
|
|
63
|
+
const key = String(name);
|
|
64
|
+
const encode = Schema.encodeUnknownEffect(
|
|
65
|
+
Schema.Struct({
|
|
66
|
+
[key]: Schema.optionalKey(
|
|
67
|
+
Schema.make<Schema.Codec<unknown, ReadonlyValue>>(type),
|
|
68
|
+
),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return [
|
|
72
|
+
key,
|
|
73
|
+
(value: unknown) =>
|
|
74
|
+
Effect.map(encode({ [key]: value }), (encoded) => encoded[key]),
|
|
75
|
+
];
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
return (patch) =>
|
|
79
|
+
pipe(
|
|
80
|
+
fieldEncoders,
|
|
81
|
+
Record.filter((_, name) => Object.hasOwn(patch, name)),
|
|
82
|
+
Record.map((encode, name) =>
|
|
83
|
+
patch[name] === undefined ? Effect.undefined : encode(patch[name]),
|
|
84
|
+
),
|
|
85
|
+
Effect.all,
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type * as ByteSize from "effect/ByteSize";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import * as Stream from "effect/Stream";
|
|
5
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
6
|
+
import * as HttpServerResponse from "effect/http/HttpServerResponse";
|
|
7
|
+
|
|
8
|
+
class BodyTooLarge extends Schema.TaggedError<BodyTooLarge>()(
|
|
9
|
+
"BodyTooLarge",
|
|
10
|
+
{},
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Count the body before downstream middleware or decoding. Configure maxBytes
|
|
15
|
+
* from the endpoint's policy. Non-identity Content-Encoding receives 415. Read
|
|
16
|
+
* arrayBuffer in the wrapped effect to verify the same bytes the decoder uses.
|
|
17
|
+
*/
|
|
18
|
+
export const withBodyLimit = <A, E, R>(
|
|
19
|
+
effect: Effect.Effect<A, E, R>,
|
|
20
|
+
options: { readonly maxBytes: ByteSize.ByteSize },
|
|
21
|
+
) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
24
|
+
const encoding = request.headers["content-encoding"]?.trim().toLowerCase();
|
|
25
|
+
if (encoding !== undefined && encoding !== "identity")
|
|
26
|
+
return HttpServerResponse.empty({ status: 415 });
|
|
27
|
+
let size = 0;
|
|
28
|
+
const chunks: Array<Uint8Array> = [];
|
|
29
|
+
const empty =
|
|
30
|
+
request.source instanceof Request && request.source.body === null;
|
|
31
|
+
const read = empty
|
|
32
|
+
? Effect.void
|
|
33
|
+
: Stream.runForEach(request.stream, (chunk) => {
|
|
34
|
+
size += chunk.byteLength;
|
|
35
|
+
if (BigInt(size) > options.maxBytes)
|
|
36
|
+
return Effect.fail(new BodyTooLarge());
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
return Effect.void;
|
|
39
|
+
});
|
|
40
|
+
const limited = yield* read.pipe(
|
|
41
|
+
Effect.as(false),
|
|
42
|
+
Effect.catchTag("BodyTooLarge", () => Effect.succeed(true)),
|
|
43
|
+
Effect.orDie,
|
|
44
|
+
);
|
|
45
|
+
if (limited) return HttpServerResponse.empty({ status: 413 });
|
|
46
|
+
const bytes = new Uint8Array(size);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
bytes.set(chunk, offset);
|
|
50
|
+
offset += chunk.byteLength;
|
|
51
|
+
}
|
|
52
|
+
const source = new Request(request.originalUrl, {
|
|
53
|
+
method: request.method,
|
|
54
|
+
headers: request.headers,
|
|
55
|
+
...(request.method === "GET" || request.method === "HEAD"
|
|
56
|
+
? {}
|
|
57
|
+
: { body: bytes }),
|
|
58
|
+
});
|
|
59
|
+
// WORKAROUND: Effect 4.0.4 caches text and bytes independently. Remove these
|
|
60
|
+
// overrides when the minimum Effect version shares text and byte reads.
|
|
61
|
+
source.arrayBuffer = () => Promise.resolve(bytes.buffer);
|
|
62
|
+
source.text = () => Promise.resolve(new TextDecoder().decode(bytes));
|
|
63
|
+
const buffered = HttpServerRequest.fromWeb(source).modify({
|
|
64
|
+
url: request.url,
|
|
65
|
+
remoteAddress: request.remoteAddress,
|
|
66
|
+
});
|
|
67
|
+
return yield* Effect.provideService(
|
|
68
|
+
effect,
|
|
69
|
+
HttpServerRequest.HttpServerRequest,
|
|
70
|
+
buffered,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as HttpRouter from "effect/http/HttpRouter";
|
|
6
|
+
import * as HttpServerError from "effect/http/HttpServerError";
|
|
7
|
+
import * as HttpServerRequest from "effect/http/HttpServerRequest";
|
|
8
|
+
|
|
9
|
+
/* oxlint-disable effecttsgo/any-unknown-in-error-context -- Preserve the router's erased request errors at the routing boundary. */
|
|
10
|
+
|
|
11
|
+
type Route = Parameters<HttpRouter.HttpRouter["addAll"]>[0][number];
|
|
12
|
+
|
|
13
|
+
const notFound = Effect.flatMap(
|
|
14
|
+
HttpServerRequest.HttpServerRequest,
|
|
15
|
+
(request) =>
|
|
16
|
+
Effect.fail(
|
|
17
|
+
new HttpServerError.HttpServerError({
|
|
18
|
+
reason: new HttpServerError.RouteNotFound({ request }),
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Effect's router adds `/a/:b` and `/a` for `/a/:b?` in one call, so an earlier
|
|
24
|
+
// `/a/:id` rejects both. Split the same way, each form claims `first` alone.
|
|
25
|
+
const optional = /(\/:[^/()]*?)\?(\/?)/;
|
|
26
|
+
const forms = (path: Route["path"]) =>
|
|
27
|
+
(optional.test(path)
|
|
28
|
+
? [path.replace(optional, "$1$2"), path.replace(optional, "$2") || "/"]
|
|
29
|
+
: [path]) as ReadonlyArray<Route["path"]>;
|
|
30
|
+
|
|
31
|
+
const missingRouteContext = `Service not found: ${HttpRouter.RouteContext.key}`;
|
|
32
|
+
|
|
33
|
+
// WORKAROUND: Effect 4.0.4 compiles endpoint middleware, decoding and the
|
|
34
|
+
// handler into one route handler. Its endpoint body reads RouteContext before
|
|
35
|
+
// decoding or calling the handler, so without RouteContext a preflight that
|
|
36
|
+
// middleware passes on stops there. Remove when Effect can run an endpoint's
|
|
37
|
+
// middleware around another handler.
|
|
38
|
+
const preflight = (handler: Route["handler"]) =>
|
|
39
|
+
handler.pipe(
|
|
40
|
+
Effect.updateContext(Context.omit(HttpRouter.RouteContext)),
|
|
41
|
+
Effect.catchDefect((defect) =>
|
|
42
|
+
defect instanceof Error && defect.message === missingRouteContext
|
|
43
|
+
? notFound
|
|
44
|
+
: Effect.die(defect),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const withOptions = <A, E, R>(
|
|
49
|
+
routes: Layer.Layer<A, E, R>,
|
|
50
|
+
endpoints: ReadonlySet<string>,
|
|
51
|
+
) =>
|
|
52
|
+
Layer.unwrap(
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
55
|
+
const registered: Array<{
|
|
56
|
+
readonly route: Route;
|
|
57
|
+
readonly prefix: string;
|
|
58
|
+
readonly context: Context.Context<never>;
|
|
59
|
+
}> = [];
|
|
60
|
+
const wrap = (
|
|
61
|
+
target: HttpRouter.HttpRouter,
|
|
62
|
+
prefix: string,
|
|
63
|
+
): HttpRouter.HttpRouter => {
|
|
64
|
+
const addAll: HttpRouter.HttpRouter["addAll"] = (entries) =>
|
|
65
|
+
Effect.contextWith((context: Context.Context<never>) => {
|
|
66
|
+
for (const route of entries)
|
|
67
|
+
registered.push({ route, prefix, context });
|
|
68
|
+
return target.addAll(entries);
|
|
69
|
+
});
|
|
70
|
+
return HttpRouter.HttpRouter.of({
|
|
71
|
+
...target,
|
|
72
|
+
addAll,
|
|
73
|
+
add: (method, path, handler, options) =>
|
|
74
|
+
addAll([HttpRouter.route(method, path, handler, options)]),
|
|
75
|
+
prefixed: (next) =>
|
|
76
|
+
wrap(target.prefixed(next), `${prefix}${next.replace(/\/$/, "")}`),
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const register = Effect.gen(function* () {
|
|
80
|
+
// Effect's routers decide which paths match: one per declared method,
|
|
81
|
+
// and `first`, which refuses a path equivalent to an earlier one.
|
|
82
|
+
const byMethod = new Map<string, HttpRouter.HttpRouter>();
|
|
83
|
+
const first = yield* HttpRouter.make;
|
|
84
|
+
const explicit = yield* HttpRouter.make;
|
|
85
|
+
for (const { route, prefix, context } of registered) {
|
|
86
|
+
const at = (target: HttpRouter.HttpRouter) =>
|
|
87
|
+
prefix === "" ? target : target.prefixed(prefix);
|
|
88
|
+
if (route.method === "OPTIONS" || route.method === "*") {
|
|
89
|
+
yield* at(explicit).addAll([{ ...route, handler: notFound }]);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const copy: Route = {
|
|
93
|
+
...route,
|
|
94
|
+
method: "OPTIONS",
|
|
95
|
+
handler: endpoints.has(`${route.method} ${route.path}`)
|
|
96
|
+
? preflight(route.handler)
|
|
97
|
+
: notFound,
|
|
98
|
+
};
|
|
99
|
+
const candidate =
|
|
100
|
+
byMethod.get(route.method) ?? (yield* HttpRouter.make);
|
|
101
|
+
byMethod.set(route.method, candidate);
|
|
102
|
+
// Route registration erases request requirements, as in Effect's router.
|
|
103
|
+
yield* Effect.provideContext(at(candidate).addAll([copy]), context);
|
|
104
|
+
// Split the prefixed path: an optional parameter may sit in the prefix.
|
|
105
|
+
const placed =
|
|
106
|
+
prefix === "" ? copy : HttpRouter.prefixRoute(copy, prefix);
|
|
107
|
+
for (const path of forms(placed.path)) {
|
|
108
|
+
const form = first.addAll([{ ...placed, path }]);
|
|
109
|
+
yield* Effect.exit(Effect.provideContext(form, context));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Effect's router answers HEAD from GET where no HEAD route matches. The
|
|
113
|
+
// root wildcard matches last, unless a HEAD route already claims it.
|
|
114
|
+
const get = byMethod.get("GET");
|
|
115
|
+
if (get !== undefined) {
|
|
116
|
+
const head = byMethod.get("HEAD") ?? (yield* HttpRouter.make);
|
|
117
|
+
byMethod.set("HEAD", head);
|
|
118
|
+
yield* Effect.exit(head.add("OPTIONS", "*", get.asHttpEffect()));
|
|
119
|
+
}
|
|
120
|
+
// An explicit OPTIONS route on the root wildcard answers every path.
|
|
121
|
+
const claimed = yield* Effect.exit(
|
|
122
|
+
explicit.add("OPTIONS", "*", notFound),
|
|
123
|
+
);
|
|
124
|
+
if (Exit.isFailure(claimed)) return;
|
|
125
|
+
const candidates = new Map(
|
|
126
|
+
Array.from(byMethod, ([method, candidate]) => [
|
|
127
|
+
method,
|
|
128
|
+
candidate.asHttpEffect(),
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
const fallback = first.asHttpEffect();
|
|
132
|
+
// The root wildcard matches last, so explicit OPTIONS routes still win.
|
|
133
|
+
yield* router.add(
|
|
134
|
+
"OPTIONS",
|
|
135
|
+
"*",
|
|
136
|
+
Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => {
|
|
137
|
+
const method = request.headers["access-control-request-method"];
|
|
138
|
+
return method === undefined
|
|
139
|
+
? fallback
|
|
140
|
+
: (candidates.get(method) ?? notFound);
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
return Layer.effectDiscard(register).pipe(
|
|
145
|
+
Layer.provideMerge(
|
|
146
|
+
routes.pipe(
|
|
147
|
+
Layer.provide(
|
|
148
|
+
Layer.succeed(HttpRouter.HttpRouter, wrap(router, "")),
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Scope from "effect/Scope";
|
|
6
|
+
import * as ConvexConfigProvider from "../ConvexConfigProvider.js";
|
|
7
|
+
|
|
8
|
+
const memoMap = Layer.makeMemoMapUnsafe();
|
|
9
|
+
export const scope = Scope.makeUnsafe();
|
|
10
|
+
const layers = new WeakMap<Layer.Layer<never>, Layer.Layer<never>>();
|
|
11
|
+
|
|
12
|
+
export const memoized = (layer: Layer.Layer<never>): Layer.Layer<never> => {
|
|
13
|
+
const cached = layers.get(layer);
|
|
14
|
+
if (cached !== undefined) return cached;
|
|
15
|
+
const built = Layer.buildWithMemoMap(layer, memoMap, scope).pipe(
|
|
16
|
+
Effect.provide(ConvexConfigProvider.layer),
|
|
17
|
+
// A failed first build is retried on the next invocation instead of failing every later one.
|
|
18
|
+
Effect.cachedWithTTL((exit) =>
|
|
19
|
+
Exit.isSuccess(exit) ? Duration.infinity : Duration.zero,
|
|
20
|
+
),
|
|
21
|
+
Effect.runSync,
|
|
22
|
+
Effect.uninterruptible,
|
|
23
|
+
Layer.effectContext,
|
|
24
|
+
);
|
|
25
|
+
layers.set(layer, built);
|
|
26
|
+
return built;
|
|
27
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { convexToJson, type Value } from "convex/values";
|
|
2
|
+
import * as Ref from "@confect/core/Ref";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Tracer from "effect/Tracer";
|
|
6
|
+
|
|
7
|
+
export const Forward = Context.Reference<
|
|
8
|
+
((span: Tracer.Span) => unknown) | undefined
|
|
9
|
+
>("@confect/server/RegisteredFunction/Forward", {
|
|
10
|
+
defaultValue: () => undefined,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const inject = (
|
|
14
|
+
ref: Ref.Any,
|
|
15
|
+
encoded: unknown,
|
|
16
|
+
context: Context.Context<never>,
|
|
17
|
+
): unknown => {
|
|
18
|
+
if (ref.reservedArgument === undefined || ref.functionType === "query")
|
|
19
|
+
return encoded;
|
|
20
|
+
const forward = Context.get(context, Forward);
|
|
21
|
+
const span = Context.getOrUndefined(context, Tracer.ParentSpan);
|
|
22
|
+
if (forward === undefined || span?._tag !== "Span") return encoded;
|
|
23
|
+
try {
|
|
24
|
+
const value = forward(span);
|
|
25
|
+
if (value === undefined) return encoded;
|
|
26
|
+
convexToJson(value as Value);
|
|
27
|
+
return {
|
|
28
|
+
...(encoded as Record<string, unknown>),
|
|
29
|
+
[ref.reservedArgument]: value,
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return encoded;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const runWithCodec = <Ref_ extends Ref.Any>(
|
|
37
|
+
ref: Ref_,
|
|
38
|
+
args: Ref.Args<Ref_>,
|
|
39
|
+
call: (
|
|
40
|
+
native: Ref.FunctionReference<Ref_>,
|
|
41
|
+
encoded: unknown,
|
|
42
|
+
) => PromiseLike<unknown>,
|
|
43
|
+
) =>
|
|
44
|
+
Effect.withFiber((fiber) =>
|
|
45
|
+
Ref.runWithCodec(ref, args, (native, encoded) =>
|
|
46
|
+
call(native, inject(ref, encoded, fiber.context)),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const encodeArgs = <Ref_ extends Ref.Any>(
|
|
51
|
+
ref: Ref_,
|
|
52
|
+
args: Ref.Args<Ref_>,
|
|
53
|
+
) =>
|
|
54
|
+
Effect.withFiberSucceed((fiber) =>
|
|
55
|
+
inject(ref, Ref.encodeArgsSync(ref, args), fiber.context),
|
|
56
|
+
);
|