@beignet/core 0.0.47 → 0.0.49
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 +33 -0
- package/README.md +81 -15
- package/dist/client/client.d.ts +0 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +28 -25
- package/dist/client/client.js.map +1 -1
- package/dist/contracts/contract-builder.d.ts +7 -2
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +20 -2
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +1 -0
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/contract-like.d.ts +2 -0
- package/dist/contracts/contract-like.d.ts.map +1 -1
- package/dist/contracts/contract-like.js +27 -1
- package/dist/contracts/contract-like.js.map +1 -1
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/query-transport.d.ts +126 -0
- package/dist/contracts/query-transport.d.ts.map +1 -0
- package/dist/contracts/query-transport.js +406 -0
- package/dist/contracts/query-transport.js.map +1 -0
- package/dist/contracts/schema-shape.d.ts +11 -0
- package/dist/contracts/schema-shape.d.ts.map +1 -1
- package/dist/contracts/schema-shape.js +13 -0
- package/dist/contracts/schema-shape.js.map +1 -1
- package/dist/contracts/types.d.ts +5 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/openapi/index.d.ts +8 -0
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +79 -5
- package/dist/openapi/index.js.map +1 -1
- package/dist/outbox/index.d.ts +8 -5
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +17 -3
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/cache.d.ts +9 -1
- package/dist/ports/cache.d.ts.map +1 -1
- package/dist/ports/cache.js +20 -5
- package/dist/ports/cache.js.map +1 -1
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js +14 -2
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/metadata.d.ts +2 -0
- package/dist/providers/metadata.d.ts.map +1 -1
- package/dist/providers/metadata.js +78 -16
- package/dist/providers/metadata.js.map +1 -1
- package/dist/query-codec.d.ts +24 -0
- package/dist/query-codec.d.ts.map +1 -1
- package/dist/query-codec.js +207 -72
- package/dist/query-codec.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +16 -8
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +31 -17
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +9 -9
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/request-preparation.d.ts.map +1 -1
- package/dist/server/request-preparation.js +11 -9
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +2 -0
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +8 -5
- package/dist/server/use-case-route.d.ts.map +1 -1
- package/dist/server/use-case-route.js +44 -17
- package/dist/server/use-case-route.js.map +1 -1
- package/package.json +3 -3
- package/skills/app-architecture/SKILL.md +27 -1
- package/src/client/client.ts +29 -28
- package/src/contracts/contract-builder.ts +32 -2
- package/src/contracts/contract-group.ts +1 -0
- package/src/contracts/contract-like.ts +40 -1
- package/src/contracts/index.ts +23 -0
- package/src/contracts/query-transport.ts +697 -0
- package/src/contracts/schema-shape.ts +24 -0
- package/src/contracts/types.ts +5 -0
- package/src/openapi/index.ts +126 -2
- package/src/outbox/index.ts +26 -5
- package/src/ports/cache.ts +29 -7
- package/src/ports/index.ts +1 -1
- package/src/providers/instrumentation.ts +15 -2
- package/src/providers/metadata.ts +132 -16
- package/src/server/hooks/rate-limit.ts +50 -24
- package/src/server/request-executor.ts +17 -9
- package/src/server/request-preparation.ts +22 -9
- package/src/server/server.ts +2 -0
- package/src/server/use-case-route.ts +62 -23
- package/src/query-codec.ts +0 -130
|
@@ -73,3 +73,27 @@ export function formatPathParamsMismatch(args: {
|
|
|
73
73
|
.filter(Boolean)
|
|
74
74
|
.join("; ");
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/** Compare an introspected query schema's fields with its transport fields. */
|
|
78
|
+
export function compareQueryTransportFields(args: {
|
|
79
|
+
schemaKeys: readonly string[];
|
|
80
|
+
transportKeys: readonly string[];
|
|
81
|
+
}): boolean {
|
|
82
|
+
const schemaKeys = [...args.schemaKeys].sort();
|
|
83
|
+
const transportKeys = [...args.transportKeys].sort();
|
|
84
|
+
return (
|
|
85
|
+
schemaKeys.length === transportKeys.length &&
|
|
86
|
+
schemaKeys.every((key, index) => key === transportKeys[index])
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Format a query schema/transport mismatch consistently across consumers. */
|
|
91
|
+
export function formatQueryTransportMismatch(args: {
|
|
92
|
+
contractName: string;
|
|
93
|
+
schemaKeys: readonly string[];
|
|
94
|
+
transportKeys: readonly string[];
|
|
95
|
+
}): string {
|
|
96
|
+
const schemaKeys = [...args.schemaKeys].sort();
|
|
97
|
+
const transportKeys = [...args.transportKeys].sort();
|
|
98
|
+
return `Query transport fields for contract "${args.contractName}" must match its query schema fields (schema: ${schemaKeys.join(", ") || "none"}; transport: ${transportKeys.join(", ") || "none"}).`;
|
|
99
|
+
}
|
package/src/contracts/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
|
2
2
|
import type { IdempotencyMeta } from "../idempotency/index.js";
|
|
3
3
|
import type { ContractDeprecationMeta } from "./lifecycle.js";
|
|
4
4
|
import type { OpenAPIOperationMeta } from "./openapi-meta.js";
|
|
5
|
+
import type { QueryTransport } from "./query-transport.js";
|
|
5
6
|
import type { RateLimitMeta } from "./rate-limit.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -343,6 +344,10 @@ export type HttpContractConfig<
|
|
|
343
344
|
* Query parameter schema.
|
|
344
345
|
*/
|
|
345
346
|
query: TQuery;
|
|
347
|
+
/**
|
|
348
|
+
* Deterministic HTTP encoding for the query schema.
|
|
349
|
+
*/
|
|
350
|
+
queryTransport: QueryTransport | null;
|
|
346
351
|
/**
|
|
347
352
|
* Request header schema or schemas.
|
|
348
353
|
*/
|
package/src/openapi/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
getContractHeaderSchemas,
|
|
12
12
|
methodSupportsRequestBody,
|
|
13
13
|
parsePathTemplate,
|
|
14
|
+
type QueryFieldTransport,
|
|
15
|
+
queryTransportSchema,
|
|
14
16
|
resolveContract,
|
|
15
17
|
STANDARD_ERROR_RESPONSE_SCHEMA,
|
|
16
18
|
} from "../contracts/index.js";
|
|
@@ -20,7 +22,9 @@ import {
|
|
|
20
22
|
} from "../contracts/lifecycle.js";
|
|
21
23
|
import {
|
|
22
24
|
comparePathParamsToTemplate,
|
|
25
|
+
compareQueryTransportFields,
|
|
23
26
|
formatPathParamsMismatch,
|
|
27
|
+
formatQueryTransportMismatch,
|
|
24
28
|
} from "../contracts/schema-shape.js";
|
|
25
29
|
import {
|
|
26
30
|
createZodIntrospector,
|
|
@@ -157,6 +161,21 @@ export interface ParameterObject {
|
|
|
157
161
|
* Parameter schema.
|
|
158
162
|
*/
|
|
159
163
|
schema?: SchemaObject | ReferenceObject;
|
|
164
|
+
/**
|
|
165
|
+
* OpenAPI parameter serialization style.
|
|
166
|
+
*/
|
|
167
|
+
style?:
|
|
168
|
+
| "form"
|
|
169
|
+
| "simple"
|
|
170
|
+
| "matrix"
|
|
171
|
+
| "label"
|
|
172
|
+
| "spaceDelimited"
|
|
173
|
+
| "pipeDelimited"
|
|
174
|
+
| "deepObject";
|
|
175
|
+
/**
|
|
176
|
+
* Whether arrays and objects are expanded into separate parameter values.
|
|
177
|
+
*/
|
|
178
|
+
explode?: boolean;
|
|
160
179
|
/**
|
|
161
180
|
* Parameter description.
|
|
162
181
|
*/
|
|
@@ -627,12 +646,36 @@ function addQueryParams(
|
|
|
627
646
|
state: GeneratorState,
|
|
628
647
|
): void {
|
|
629
648
|
if (!contract.query) return;
|
|
649
|
+
const transport = contract.queryTransport;
|
|
650
|
+
if (!transport) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`Contract "${contract.name}" declares a query schema without a query transport.`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
630
655
|
|
|
631
656
|
const shape = state.introspector.getShape(contract.query);
|
|
632
|
-
if (!shape)
|
|
657
|
+
if (!shape) {
|
|
658
|
+
throw new Error(
|
|
659
|
+
`Unable to inspect query schema for contract "${contract.name}". Pass a schemaIntrospector that can read its fields so OpenAPI requiredness and constraints remain aligned with the declared query transport.`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
633
662
|
|
|
634
|
-
|
|
663
|
+
const schemaKeys = Object.keys(shape);
|
|
664
|
+
const transportKeys = Object.keys(transport.fields);
|
|
665
|
+
if (!compareQueryTransportFields({ schemaKeys, transportKeys })) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
formatQueryTransportMismatch({
|
|
668
|
+
contractName: contract.name,
|
|
669
|
+
schemaKeys,
|
|
670
|
+
transportKeys,
|
|
671
|
+
}),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const key of Object.keys(transport.fields)) {
|
|
635
676
|
const originalField = shape[key];
|
|
677
|
+
const fieldTransport = transport.fields[key];
|
|
678
|
+
if (!originalField || !fieldTransport) continue;
|
|
636
679
|
const optional = state.introspector.isOptional(originalField);
|
|
637
680
|
|
|
638
681
|
const field = originalField;
|
|
@@ -644,6 +687,7 @@ function addQueryParams(
|
|
|
644
687
|
state,
|
|
645
688
|
"input",
|
|
646
689
|
);
|
|
690
|
+
applyQueryTransportSchema(paramSchemaRef, fieldTransport, state);
|
|
647
691
|
|
|
648
692
|
const param: ParameterObject = {
|
|
649
693
|
name: key,
|
|
@@ -651,12 +695,92 @@ function addQueryParams(
|
|
|
651
695
|
required: !optional,
|
|
652
696
|
schema: paramSchemaRef,
|
|
653
697
|
description,
|
|
698
|
+
style: fieldTransport.kind === "deep-object" ? "deepObject" : "form",
|
|
699
|
+
explode: true,
|
|
654
700
|
};
|
|
655
701
|
|
|
656
702
|
addParameter(operation, param);
|
|
657
703
|
}
|
|
658
704
|
}
|
|
659
705
|
|
|
706
|
+
function applyQueryTransportSchema(
|
|
707
|
+
schema: SchemaObject | ReferenceObject,
|
|
708
|
+
transport: QueryFieldTransport,
|
|
709
|
+
state: GeneratorState,
|
|
710
|
+
): void {
|
|
711
|
+
if (!("$ref" in schema) || typeof schema.$ref !== "string") return;
|
|
712
|
+
const prefix = "#/components/schemas/";
|
|
713
|
+
if (!schema.$ref.startsWith(prefix)) return;
|
|
714
|
+
const name = schema.$ref.slice(prefix.length);
|
|
715
|
+
const converted = state.components.schemas?.[name];
|
|
716
|
+
if (!converted || !state.components.schemas) return;
|
|
717
|
+
state.components.schemas[name] = mergeQueryTransportSchema(
|
|
718
|
+
converted,
|
|
719
|
+
transport,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function mergeQueryTransportSchema(
|
|
724
|
+
converted: SchemaObject,
|
|
725
|
+
transport: QueryFieldTransport,
|
|
726
|
+
): SchemaObject {
|
|
727
|
+
const declared = queryTransportSchema(transport);
|
|
728
|
+
const merged: SchemaObject = {
|
|
729
|
+
...converted,
|
|
730
|
+
type: declared.type,
|
|
731
|
+
};
|
|
732
|
+
if (declared.format) merged.format = declared.format;
|
|
733
|
+
if (transport.kind === "integer") {
|
|
734
|
+
merged.minimum = Math.max(
|
|
735
|
+
typeof converted.minimum === "number"
|
|
736
|
+
? converted.minimum
|
|
737
|
+
: Number.MIN_SAFE_INTEGER,
|
|
738
|
+
Number.MIN_SAFE_INTEGER,
|
|
739
|
+
);
|
|
740
|
+
merged.maximum = Math.min(
|
|
741
|
+
typeof converted.maximum === "number"
|
|
742
|
+
? converted.maximum
|
|
743
|
+
: Number.MAX_SAFE_INTEGER,
|
|
744
|
+
Number.MAX_SAFE_INTEGER,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
if (declared["x-beignet-empty-query"]) {
|
|
748
|
+
merged["x-beignet-empty-query"] = declared["x-beignet-empty-query"];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (transport.kind === "array") {
|
|
752
|
+
const convertedItems =
|
|
753
|
+
typeof converted.items === "object" && converted.items !== null
|
|
754
|
+
? (converted.items as SchemaObject)
|
|
755
|
+
: {};
|
|
756
|
+
merged.items = mergeQueryTransportSchema(convertedItems, transport.item);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
if (transport.kind === "deep-object") {
|
|
760
|
+
const convertedProperties =
|
|
761
|
+
typeof converted.properties === "object" && converted.properties !== null
|
|
762
|
+
? (converted.properties as Record<string, unknown>)
|
|
763
|
+
: {};
|
|
764
|
+
merged.properties = Object.fromEntries(
|
|
765
|
+
Object.entries(transport.fields).map(([key, field]) => {
|
|
766
|
+
const convertedProperty = convertedProperties[key];
|
|
767
|
+
return [
|
|
768
|
+
key,
|
|
769
|
+
mergeQueryTransportSchema(
|
|
770
|
+
typeof convertedProperty === "object" && convertedProperty !== null
|
|
771
|
+
? (convertedProperty as SchemaObject)
|
|
772
|
+
: {},
|
|
773
|
+
field,
|
|
774
|
+
),
|
|
775
|
+
];
|
|
776
|
+
}),
|
|
777
|
+
);
|
|
778
|
+
merged.additionalProperties = false;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return merged;
|
|
782
|
+
}
|
|
783
|
+
|
|
660
784
|
/**
|
|
661
785
|
* Add header parameters from contract to operation.
|
|
662
786
|
*/
|
package/src/outbox/index.ts
CHANGED
|
@@ -555,7 +555,8 @@ export interface DrainOutboxOptions {
|
|
|
555
555
|
*/
|
|
556
556
|
registry: OutboxRegistry;
|
|
557
557
|
/**
|
|
558
|
-
* Event bus used for event messages.
|
|
558
|
+
* Event bus used for event messages. Required when the registry contains
|
|
559
|
+
* events.
|
|
559
560
|
*/
|
|
560
561
|
eventBus?: {
|
|
561
562
|
publish<E extends EventPayloadDef>(
|
|
@@ -565,7 +566,8 @@ export interface DrainOutboxOptions {
|
|
|
565
566
|
): MaybePromise<void>;
|
|
566
567
|
};
|
|
567
568
|
/**
|
|
568
|
-
* Job dispatcher used for job messages.
|
|
569
|
+
* Job dispatcher used for job messages. Required when the registry contains
|
|
570
|
+
* jobs.
|
|
569
571
|
*/
|
|
570
572
|
jobs?: JobDispatcherPort;
|
|
571
573
|
/**
|
|
@@ -1476,15 +1478,17 @@ async function deliverOutboxMessage(
|
|
|
1476
1478
|
* Claim and deliver one batch of outbox messages.
|
|
1477
1479
|
*
|
|
1478
1480
|
* This does not loop forever; production workers should call it on their own
|
|
1479
|
-
* polling cadence. Event and job messages require matching registry entries
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1481
|
+
* polling cadence. Event and job messages require matching registry entries
|
|
1482
|
+
* and delivery transports. Required transports are validated before a batch
|
|
1483
|
+
* is claimed. Failed messages are retried with backoff until `maxAttempts`,
|
|
1484
|
+
* then dead-lettered.
|
|
1482
1485
|
*/
|
|
1483
1486
|
export async function drainOutbox(
|
|
1484
1487
|
options: DrainOutboxOptions,
|
|
1485
1488
|
): Promise<DrainOutboxResult> {
|
|
1486
1489
|
const batchSize = options.batchSize ?? 100;
|
|
1487
1490
|
assertPositiveInteger("batchSize", batchSize);
|
|
1491
|
+
assertOutboxDeliveryCapabilities(options);
|
|
1488
1492
|
const instrumentation = createProviderInstrumentation(
|
|
1489
1493
|
options.instrumentation,
|
|
1490
1494
|
{
|
|
@@ -1659,6 +1663,23 @@ export async function drainOutbox(
|
|
|
1659
1663
|
return result;
|
|
1660
1664
|
}
|
|
1661
1665
|
|
|
1666
|
+
function assertOutboxDeliveryCapabilities(options: DrainOutboxOptions): void {
|
|
1667
|
+
const missing: string[] = [];
|
|
1668
|
+
|
|
1669
|
+
if (options.registry.events.size > 0 && !options.eventBus) {
|
|
1670
|
+
missing.push("events require an event bus");
|
|
1671
|
+
}
|
|
1672
|
+
if (options.registry.jobs.size > 0 && !options.jobs) {
|
|
1673
|
+
missing.push("jobs require a job dispatcher");
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
if (missing.length > 0) {
|
|
1677
|
+
throw new OutboxRegistryError(
|
|
1678
|
+
`Cannot drain this outbox registry: ${missing.join("; ")}.`,
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1662
1683
|
/**
|
|
1663
1684
|
* Domain event recorder port re-exported for outbox integrations.
|
|
1664
1685
|
*/
|
package/src/ports/cache.ts
CHANGED
|
@@ -3,11 +3,35 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export interface CacheSetOptions {
|
|
5
5
|
/**
|
|
6
|
-
* Time-to-live in seconds.
|
|
6
|
+
* Time-to-live in seconds. Values must be positive safe integers. Omit this
|
|
7
|
+
* for a value that does not expire.
|
|
7
8
|
*/
|
|
8
9
|
ttlSeconds?: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Resolve and validate the optional TTL shared by cache adapters.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Cache write options.
|
|
16
|
+
* @returns The positive TTL, or `undefined` for a persistent value.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveCacheTtlSeconds(
|
|
19
|
+
options?: CacheSetOptions,
|
|
20
|
+
): number | undefined {
|
|
21
|
+
const ttlSeconds = options?.ttlSeconds;
|
|
22
|
+
if (ttlSeconds === undefined) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
27
|
+
throw new RangeError(
|
|
28
|
+
"Cache ttlSeconds must be a positive safe integer when provided.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ttlSeconds;
|
|
33
|
+
}
|
|
34
|
+
|
|
11
35
|
/**
|
|
12
36
|
* App-facing string cache port.
|
|
13
37
|
*
|
|
@@ -54,15 +78,12 @@ type MemoryCacheEntry = {
|
|
|
54
78
|
};
|
|
55
79
|
|
|
56
80
|
function resolveExpiresAt(options: CacheSetOptions | undefined): number | null {
|
|
57
|
-
|
|
81
|
+
const ttlSeconds = resolveCacheTtlSeconds(options);
|
|
82
|
+
if (ttlSeconds === undefined) {
|
|
58
83
|
return null;
|
|
59
84
|
}
|
|
60
85
|
|
|
61
|
-
|
|
62
|
-
return Date.now();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return Date.now() + options.ttlSeconds * 1000;
|
|
86
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Date.now() + ttlSeconds * 1000);
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
function isExpired(entry: MemoryCacheEntry): boolean {
|
|
@@ -120,6 +141,7 @@ export function createMemoryCache(
|
|
|
120
141
|
return (await getFreshEntry(key)) != null;
|
|
121
142
|
},
|
|
122
143
|
async remember(key, factory, options) {
|
|
144
|
+
resolveCacheTtlSeconds(options);
|
|
123
145
|
const cached = await cache.get(key);
|
|
124
146
|
if (cached != null) {
|
|
125
147
|
return cached;
|
package/src/ports/index.ts
CHANGED
|
@@ -483,7 +483,7 @@ export type { CachePort, CacheSetOptions } from "./cache.js";
|
|
|
483
483
|
/**
|
|
484
484
|
* Cache helper exports.
|
|
485
485
|
*/
|
|
486
|
-
export { createMemoryCache } from "./cache.js";
|
|
486
|
+
export { createMemoryCache, resolveCacheTtlSeconds } from "./cache.js";
|
|
487
487
|
/**
|
|
488
488
|
* Clock port exports.
|
|
489
489
|
*/
|
|
@@ -502,7 +502,11 @@ export function createProviderInstrumentation(
|
|
|
502
502
|
function isEnabled(watcher = options.watcher): boolean {
|
|
503
503
|
if (!port) return false;
|
|
504
504
|
if (!watcher) return true;
|
|
505
|
-
|
|
505
|
+
try {
|
|
506
|
+
return port.isWatcherEnabled?.(watcher) ?? true;
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
506
510
|
}
|
|
507
511
|
|
|
508
512
|
function record(event: ProviderInstrumentationEventInput): unknown {
|
|
@@ -530,7 +534,16 @@ export function createProviderInstrumentation(
|
|
|
530
534
|
}
|
|
531
535
|
|
|
532
536
|
try {
|
|
533
|
-
|
|
537
|
+
const result = port.record(redacted);
|
|
538
|
+
if (
|
|
539
|
+
result !== null &&
|
|
540
|
+
(typeof result === "object" || typeof result === "function") &&
|
|
541
|
+
"then" in result &&
|
|
542
|
+
typeof result.then === "function"
|
|
543
|
+
) {
|
|
544
|
+
return Promise.resolve(result).catch(() => undefined);
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
534
547
|
} catch {
|
|
535
548
|
return undefined;
|
|
536
549
|
}
|
|
@@ -24,6 +24,7 @@ export type ProviderPackageVariantMetadata = {
|
|
|
24
24
|
displayName?: string;
|
|
25
25
|
env?: readonly string[];
|
|
26
26
|
requiredEnv?: readonly string[];
|
|
27
|
+
requiredEnvAlternatives?: readonly (readonly string[])[];
|
|
27
28
|
requiredTables?: readonly string[];
|
|
28
29
|
registration?: ProviderPackageRegistrationMetadata;
|
|
29
30
|
};
|
|
@@ -34,6 +35,7 @@ export type ProviderPackageMetadata = {
|
|
|
34
35
|
appPorts?: readonly ProviderPackagePortMetadata[];
|
|
35
36
|
env?: readonly string[];
|
|
36
37
|
requiredEnv?: readonly string[];
|
|
38
|
+
requiredEnvAlternatives?: readonly (readonly string[])[];
|
|
37
39
|
requiredTables?: readonly string[];
|
|
38
40
|
registration?: ProviderPackageRegistrationMetadata;
|
|
39
41
|
variants?: readonly ProviderPackageVariantMetadata[];
|
|
@@ -91,17 +93,43 @@ export function parseProviderPackageMetadata(
|
|
|
91
93
|
);
|
|
92
94
|
if (requiredEnv !== undefined) {
|
|
93
95
|
metadata.requiredEnv = requiredEnv;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
validateRequiredEnvMembers(
|
|
97
|
+
requiredEnv,
|
|
98
|
+
env,
|
|
99
|
+
"beignet.provider.requiredEnv",
|
|
100
|
+
"beignet.provider.env",
|
|
101
|
+
issues,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const requiredEnvAlternatives = parseStringArrayArray(
|
|
106
|
+
input.requiredEnvAlternatives,
|
|
107
|
+
"beignet.provider.requiredEnvAlternatives",
|
|
108
|
+
issues,
|
|
109
|
+
);
|
|
110
|
+
if (requiredEnvAlternatives !== undefined) {
|
|
111
|
+
metadata.requiredEnvAlternatives = requiredEnvAlternatives;
|
|
112
|
+
for (const alternative of requiredEnvAlternatives) {
|
|
113
|
+
validateRequiredEnvMembers(
|
|
114
|
+
alternative,
|
|
115
|
+
env,
|
|
116
|
+
"beignet.provider.requiredEnvAlternatives",
|
|
117
|
+
"beignet.provider.env",
|
|
118
|
+
issues,
|
|
119
|
+
);
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
122
|
|
|
123
|
+
if (
|
|
124
|
+
input.requiredEnv !== undefined &&
|
|
125
|
+
input.requiredEnvAlternatives !== undefined
|
|
126
|
+
) {
|
|
127
|
+
issues.push({
|
|
128
|
+
path: "beignet.provider.requiredEnvAlternatives",
|
|
129
|
+
message: "must not be set alongside beignet.provider.requiredEnv",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
const requiredTables = parseStringArray(
|
|
106
134
|
input.requiredTables,
|
|
107
135
|
"beignet.provider.requiredTables",
|
|
@@ -137,6 +165,13 @@ export function parseProviderPackageMetadata(
|
|
|
137
165
|
"must not be set when beignet.provider.variants is present; declare requiredEnv on each variant instead",
|
|
138
166
|
});
|
|
139
167
|
}
|
|
168
|
+
if (input.requiredEnvAlternatives !== undefined) {
|
|
169
|
+
issues.push({
|
|
170
|
+
path: "beignet.provider.requiredEnvAlternatives",
|
|
171
|
+
message:
|
|
172
|
+
"must not be set when beignet.provider.variants is present; declare requiredEnvAlternatives on each variant instead",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
140
175
|
if (input.registration !== undefined) {
|
|
141
176
|
issues.push({
|
|
142
177
|
path: "beignet.provider.registration",
|
|
@@ -186,6 +221,60 @@ function parseStringArray(
|
|
|
186
221
|
return parsed;
|
|
187
222
|
}
|
|
188
223
|
|
|
224
|
+
function parseStringArrayArray(
|
|
225
|
+
value: unknown,
|
|
226
|
+
path: string,
|
|
227
|
+
issues: ProviderPackageMetadataIssue[],
|
|
228
|
+
): string[][] | undefined {
|
|
229
|
+
if (value === undefined) return undefined;
|
|
230
|
+
if (!Array.isArray(value)) {
|
|
231
|
+
issues.push({ path, message: "must be an array of string arrays" });
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
if (value.length === 0) {
|
|
235
|
+
issues.push({
|
|
236
|
+
path,
|
|
237
|
+
message: "must include at least one alternative when present",
|
|
238
|
+
});
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const parsed: string[][] = [];
|
|
243
|
+
for (const [index, entry] of value.entries()) {
|
|
244
|
+
const entryPath = `${path}[${index}]`;
|
|
245
|
+
const alternative = parseStringArray(entry, entryPath, issues);
|
|
246
|
+
if (alternative === undefined) continue;
|
|
247
|
+
if (alternative.length === 0) {
|
|
248
|
+
issues.push({
|
|
249
|
+
path: entryPath,
|
|
250
|
+
message: "must include at least one environment variable",
|
|
251
|
+
});
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
parsed.push(alternative);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return parsed;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function validateRequiredEnvMembers(
|
|
261
|
+
requiredEnv: readonly string[],
|
|
262
|
+
env: readonly string[] | undefined,
|
|
263
|
+
path: string,
|
|
264
|
+
envPath: string,
|
|
265
|
+
issues: ProviderPackageMetadataIssue[],
|
|
266
|
+
): void {
|
|
267
|
+
const envSet = new Set(env ?? []);
|
|
268
|
+
for (const envVar of requiredEnv) {
|
|
269
|
+
if (!envSet.has(envVar)) {
|
|
270
|
+
issues.push({
|
|
271
|
+
path,
|
|
272
|
+
message: `${envVar} must also be listed in ${envPath}`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
189
278
|
function parseAppPorts(
|
|
190
279
|
value: unknown,
|
|
191
280
|
issues: ProviderPackageMetadataIssue[],
|
|
@@ -280,16 +369,40 @@ function parseVariants(
|
|
|
280
369
|
issues,
|
|
281
370
|
);
|
|
282
371
|
if (requiredEnv !== undefined) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
372
|
+
validateRequiredEnvMembers(
|
|
373
|
+
requiredEnv,
|
|
374
|
+
env,
|
|
375
|
+
`${entryPath}.requiredEnv`,
|
|
376
|
+
`${entryPath}.env`,
|
|
377
|
+
issues,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const requiredEnvAlternatives = parseStringArrayArray(
|
|
382
|
+
entry.requiredEnvAlternatives,
|
|
383
|
+
`${entryPath}.requiredEnvAlternatives`,
|
|
384
|
+
issues,
|
|
385
|
+
);
|
|
386
|
+
if (requiredEnvAlternatives !== undefined) {
|
|
387
|
+
for (const alternative of requiredEnvAlternatives) {
|
|
388
|
+
validateRequiredEnvMembers(
|
|
389
|
+
alternative,
|
|
390
|
+
env,
|
|
391
|
+
`${entryPath}.requiredEnvAlternatives`,
|
|
392
|
+
`${entryPath}.env`,
|
|
393
|
+
issues,
|
|
394
|
+
);
|
|
291
395
|
}
|
|
292
396
|
}
|
|
397
|
+
if (
|
|
398
|
+
entry.requiredEnv !== undefined &&
|
|
399
|
+
entry.requiredEnvAlternatives !== undefined
|
|
400
|
+
) {
|
|
401
|
+
issues.push({
|
|
402
|
+
path: `${entryPath}.requiredEnvAlternatives`,
|
|
403
|
+
message: `must not be set alongside ${entryPath}.requiredEnv`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
293
406
|
|
|
294
407
|
const requiredTables = parseStringArray(
|
|
295
408
|
entry.requiredTables,
|
|
@@ -308,6 +421,9 @@ function parseVariants(
|
|
|
308
421
|
if (displayName !== undefined) variant.displayName = displayName;
|
|
309
422
|
if (env !== undefined) variant.env = env;
|
|
310
423
|
if (requiredEnv !== undefined) variant.requiredEnv = requiredEnv;
|
|
424
|
+
if (requiredEnvAlternatives !== undefined) {
|
|
425
|
+
variant.requiredEnvAlternatives = requiredEnvAlternatives;
|
|
426
|
+
}
|
|
311
427
|
if (requiredTables !== undefined) variant.requiredTables = requiredTables;
|
|
312
428
|
if (registration !== undefined) variant.registration = registration;
|
|
313
429
|
parsed.push(variant);
|