@nymbalhq/types 0.2.0 → 1.1.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/dist/index.d.ts +270 -77
- package/dist/index.js +284 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ declare class ConfigError extends NymbalError {
|
|
|
17
17
|
constructor(message: string, options?: {
|
|
18
18
|
cause?: unknown;
|
|
19
19
|
context?: ErrorContext;
|
|
20
|
-
code?: `config.${string}`;
|
|
20
|
+
code?: `config.${string}` | `adapter.${string}`;
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
declare class ValidationError extends NymbalError {
|
|
@@ -142,6 +142,8 @@ interface Product {
|
|
|
142
142
|
declare const PRODUCT_SORT_KEYS: readonly ["newest", "price-asc", "price-desc", "name-asc", "name-desc"];
|
|
143
143
|
type ProductSortKey = (typeof PRODUCT_SORT_KEYS)[number];
|
|
144
144
|
declare const DEFAULT_PRODUCT_SORT: ProductSortKey;
|
|
145
|
+
/** What a storefront shows for each sort. */
|
|
146
|
+
declare const PRODUCT_SORT_LABELS: Readonly<Record<ProductSortKey, string>>;
|
|
145
147
|
|
|
146
148
|
interface Category {
|
|
147
149
|
id: string;
|
|
@@ -644,18 +646,140 @@ interface NymbalEvent<T = unknown> {
|
|
|
644
646
|
}
|
|
645
647
|
type EventHandler<T = unknown> = (event: NymbalEvent<T>) => Promise<void> | void;
|
|
646
648
|
|
|
649
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
650
|
+
interface RequestAuth {
|
|
651
|
+
userId?: string;
|
|
652
|
+
roles?: string[];
|
|
653
|
+
token?: string;
|
|
654
|
+
}
|
|
655
|
+
interface Logger {
|
|
656
|
+
trace(obj: unknown, msg?: string): void;
|
|
657
|
+
debug(obj: unknown, msg?: string): void;
|
|
658
|
+
info(obj: unknown, msg?: string): void;
|
|
659
|
+
warn(obj: unknown, msg?: string): void;
|
|
660
|
+
error(obj: unknown, msg?: string): void;
|
|
661
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
662
|
+
}
|
|
663
|
+
interface RequestContext<TBody = unknown, TQuery = Record<string, string | string[] | undefined>, TParams = Record<string, string>> {
|
|
664
|
+
method: HttpMethod;
|
|
665
|
+
path: string;
|
|
666
|
+
url: string;
|
|
667
|
+
params: TParams;
|
|
668
|
+
query: TQuery;
|
|
669
|
+
body: TBody;
|
|
670
|
+
rawBody?: Buffer;
|
|
671
|
+
headers: Record<string, string | string[] | undefined>;
|
|
672
|
+
cookies: Record<string, string>;
|
|
673
|
+
auth?: RequestAuth;
|
|
674
|
+
requestId: string;
|
|
675
|
+
correlationId: string;
|
|
676
|
+
logger: Logger;
|
|
677
|
+
ip: string;
|
|
678
|
+
}
|
|
679
|
+
interface ResponseCookie {
|
|
680
|
+
name: string;
|
|
681
|
+
value: string;
|
|
682
|
+
path?: string;
|
|
683
|
+
domain?: string;
|
|
684
|
+
maxAge?: number;
|
|
685
|
+
httpOnly?: boolean;
|
|
686
|
+
secure?: boolean;
|
|
687
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
688
|
+
}
|
|
689
|
+
interface ResponseEnvelope<T = unknown> {
|
|
690
|
+
status: number;
|
|
691
|
+
headers?: Record<string, string>;
|
|
692
|
+
body?: T;
|
|
693
|
+
cookies?: ResponseCookie[];
|
|
694
|
+
}
|
|
695
|
+
type RouteHandler<TBody = unknown, TQuery = Record<string, string | string[] | undefined>, TParams = Record<string, string>, TResponse = unknown> = (ctx: RequestContext<TBody, TQuery, TParams>) => Promise<ResponseEnvelope<TResponse>> | ResponseEnvelope<TResponse>;
|
|
696
|
+
interface RouteOptions {
|
|
697
|
+
name?: string;
|
|
698
|
+
csrf?: boolean;
|
|
699
|
+
rateLimit?: string;
|
|
700
|
+
rawBody?: boolean;
|
|
701
|
+
}
|
|
702
|
+
type MiddlewareHandler = (ctx: RequestContext) => Promise<ResponseEnvelope | void> | ResponseEnvelope | void;
|
|
703
|
+
interface SseEvent {
|
|
704
|
+
id?: string;
|
|
705
|
+
event?: string;
|
|
706
|
+
data: unknown;
|
|
707
|
+
}
|
|
708
|
+
interface StreamContext {
|
|
709
|
+
send(event: SseEvent): void;
|
|
710
|
+
close(): void;
|
|
711
|
+
onClose(callback: () => void): void;
|
|
712
|
+
}
|
|
713
|
+
type StreamHandler = (ctx: RequestContext, stream: StreamContext) => Promise<void> | void;
|
|
714
|
+
interface HttpAdapter {
|
|
715
|
+
registerRoute(method: HttpMethod, path: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
716
|
+
registerStreamRoute(method: 'GET', path: string, handler: StreamHandler, options?: RouteOptions): void;
|
|
717
|
+
registerMiddleware(middleware: MiddlewareHandler): void;
|
|
718
|
+
setDefaultHeaders(headers: Record<string, string>): void;
|
|
719
|
+
start(port: number, host?: string): Promise<void>;
|
|
720
|
+
stop(): Promise<void>;
|
|
721
|
+
}
|
|
722
|
+
|
|
647
723
|
type HealthStatus = 'healthy' | 'degraded' | 'down';
|
|
648
724
|
interface HealthReport {
|
|
649
725
|
status: HealthStatus;
|
|
650
726
|
detail?: string;
|
|
651
727
|
}
|
|
728
|
+
/** How long a health check may run before it reports `down`. */
|
|
729
|
+
declare const HEALTH_CHECK_TIMEOUT_MS = 5000;
|
|
730
|
+
interface BoundedHealthCheckOptions {
|
|
731
|
+
/** `HEALTH_CHECK_TIMEOUT_MS` when not given. */
|
|
732
|
+
timeoutMs?: number;
|
|
733
|
+
/** Values an error's message must never show (a URL's user, password or query values); each becomes `***`. */
|
|
734
|
+
redact?: readonly string[];
|
|
735
|
+
}
|
|
736
|
+
/** An error's message for a report; an AggregateError (a refused connection to every address of a name) lists its own. */
|
|
737
|
+
declare function errorText(error: unknown): string;
|
|
738
|
+
/**
|
|
739
|
+
* The shortest value worth removing from text as a secret. A shorter one is too likely to be an ordinary word (a local
|
|
740
|
+
* password of `nymbal`), whose removal garbles the text; user info and query strings go with the URL rewrite anyway.
|
|
741
|
+
*/
|
|
742
|
+
declare const SECRET_MIN_LENGTH = 8;
|
|
743
|
+
/**
|
|
744
|
+
* The values in a connection URL that are credentials or may carry one: its password and its query values, and its
|
|
745
|
+
* user unless `user` is false, each as written and decoded, if at least `SECRET_MIN_LENGTH` long. A user is worth
|
|
746
|
+
* hiding in a driver's error, not in our own text.
|
|
747
|
+
*/
|
|
748
|
+
declare function connectionSecrets(url: string, options?: {
|
|
749
|
+
user?: boolean;
|
|
750
|
+
}): string[];
|
|
751
|
+
/**
|
|
752
|
+
* A connection URL as it may be printed: `scheme://host:port/path`, with no user, password, query or fragment. Text
|
|
753
|
+
* that is not a URL comes back as `<unparsable url>`, since it may be a URL with a secret the parser could not read.
|
|
754
|
+
*/
|
|
755
|
+
declare function displayUrl(url: string): string;
|
|
756
|
+
/**
|
|
757
|
+
* Runs `check` within the bound. A check that throws is `down` with the error's message, every `redact` value in it
|
|
758
|
+
* replaced; one still running when the bound passes is `down` with `timed out after <n> ms`, and its signal aborted.
|
|
759
|
+
*/
|
|
760
|
+
declare function boundedHealthCheck(check: (signal: AbortSignal) => Promise<HealthReport> | HealthReport, options?: BoundedHealthCheckOptions): Promise<HealthReport>;
|
|
652
761
|
interface AdapterBase {
|
|
653
|
-
|
|
762
|
+
/** What this adapter offers, for the app's per-slot report; each adapter answers for itself. */
|
|
763
|
+
capabilities(): readonly string[];
|
|
654
764
|
readonly producesEvents: readonly string[];
|
|
655
765
|
readonly consumesEvents: readonly string[];
|
|
656
766
|
initialize(config: unknown): Promise<void>;
|
|
657
767
|
healthCheck(): Promise<HealthReport>;
|
|
658
768
|
}
|
|
769
|
+
/** The commerce slots a store's config names a provider for. */
|
|
770
|
+
type CommerceSlot = 'payments' | 'email' | 'reviews' | 'search' | 'analytics' | 'shipping' | 'tax' | 'ai';
|
|
771
|
+
/** Every slot an adapter package can fill: the commerce slots and the infrastructure primitives. */
|
|
772
|
+
type Slot = CommerceSlot | 'documentStore' | 'eventBus' | 'media';
|
|
773
|
+
/**
|
|
774
|
+
* What the kernel hands an adapter package's `createAdapter`. A package that serves more than one slot switches on
|
|
775
|
+
* `slot`; `config` is a commerce slot's `config` block from nymbal.config.ts, or a primitive's connection block.
|
|
776
|
+
*/
|
|
777
|
+
interface AdapterContext {
|
|
778
|
+
slot: Slot;
|
|
779
|
+
config: Record<string, unknown>;
|
|
780
|
+
logger: Logger;
|
|
781
|
+
env: NodeJS.ProcessEnv;
|
|
782
|
+
}
|
|
659
783
|
|
|
660
784
|
interface PutOptions {
|
|
661
785
|
ttl?: number;
|
|
@@ -674,8 +798,9 @@ interface QueryParams {
|
|
|
674
798
|
value: unknown;
|
|
675
799
|
};
|
|
676
800
|
/**
|
|
677
|
-
* Orders the results without filtering them; `field` may be a dotted path (`priceRange.minMinor`).
|
|
678
|
-
*
|
|
801
|
+
* Orders the results without filtering them; `field` may be a dotted path (`priceRange.minMinor`). Values compare
|
|
802
|
+
* with `compareDocumentValues`; null and missing values come last in either direction, and ties break by document
|
|
803
|
+
* key, ascending, so the order is total. A cursor issued under one `orderBy` is refused under another.
|
|
679
804
|
*/
|
|
680
805
|
orderBy?: {
|
|
681
806
|
field: string;
|
|
@@ -691,6 +816,7 @@ interface QueryResult<T> {
|
|
|
691
816
|
nextCursor: string | null;
|
|
692
817
|
}
|
|
693
818
|
interface DocumentStoreAdapter {
|
|
819
|
+
capabilities(): readonly string[];
|
|
694
820
|
get<T>(collection: string, key: string): Promise<T | null>;
|
|
695
821
|
put<T>(collection: string, key: string, document: T, options?: PutOptions): Promise<void>;
|
|
696
822
|
delete(collection: string, key: string): Promise<void>;
|
|
@@ -698,8 +824,30 @@ interface DocumentStoreAdapter {
|
|
|
698
824
|
batchGet<T>(collection: string, keys: string[]): Promise<Map<string, T>>;
|
|
699
825
|
batchPut<T>(collection: string, items: Map<string, T>): Promise<void>;
|
|
700
826
|
close(): Promise<void>;
|
|
827
|
+
/**
|
|
828
|
+
* Whether the store can serve, within `HEALTH_CHECK_TIMEOUT_MS`. Optional, so a store written before it existed
|
|
829
|
+
* still type-checks; `nymbal doctor` reports one without it as `no healthCheck`.
|
|
830
|
+
*/
|
|
831
|
+
healthCheck?(): Promise<HealthReport>;
|
|
701
832
|
}
|
|
702
833
|
|
|
834
|
+
/**
|
|
835
|
+
* A total order over document values: numbers numerically, strings by `Intl.Collator` at base sensitivity (case and
|
|
836
|
+
* accents tie), other types after strings, and null or missing last. Values of different types rank by type first.
|
|
837
|
+
*/
|
|
838
|
+
declare function compareDocumentValues(a: unknown, b: unknown): number;
|
|
839
|
+
type SortKeyCondition = Pick<NonNullable<QueryParams['sortKey']>, 'operator' | 'value'>;
|
|
840
|
+
/** `eq` is `===`; `gt`, `lt` and `between` (inclusive, `[low, high]`) are `compareDocumentValues`. */
|
|
841
|
+
declare function matchesSortKey(value: unknown, condition: SortKeyCondition): boolean;
|
|
842
|
+
/** The value at a dotted path (`priceRange.minMinor`); missing anywhere along it is `undefined`. */
|
|
843
|
+
declare function documentValueAt(doc: unknown, path: string): unknown;
|
|
844
|
+
declare const DEFAULT_QUERY_LIMIT = 50;
|
|
845
|
+
/**
|
|
846
|
+
* One page of `params` over the documents of a collection: filter by partition (`'*'` for all) and sort key, order,
|
|
847
|
+
* then seek past the cursor by value, so a document deleted between pages neither restarts nor repeats the walk.
|
|
848
|
+
*/
|
|
849
|
+
declare function queryDocuments<T>(documents: Iterable<readonly [string, unknown]>, params: QueryParams): QueryResult<T>;
|
|
850
|
+
|
|
703
851
|
interface RetryPolicy {
|
|
704
852
|
maxAttempts: number;
|
|
705
853
|
backoff: 'exponential' | 'linear' | 'fixed';
|
|
@@ -709,6 +857,10 @@ interface SubscriptionConfig {
|
|
|
709
857
|
retryPolicy?: RetryPolicy;
|
|
710
858
|
deadLetterQueue?: string;
|
|
711
859
|
batchSize?: number;
|
|
860
|
+
/**
|
|
861
|
+
* A named subscription is handled once per name across every bus instance (one consumer group); an unnamed one is
|
|
862
|
+
* handled by every subscriber on every instance. On a single-process bus the two are the same.
|
|
863
|
+
*/
|
|
712
864
|
name?: string;
|
|
713
865
|
/** The subscriber only maintains derived state (a projection, a search index): it also receives `replay`. */
|
|
714
866
|
derived?: boolean;
|
|
@@ -718,7 +870,16 @@ interface Subscription {
|
|
|
718
870
|
patterns: string[];
|
|
719
871
|
unsubscribe(): Promise<void>;
|
|
720
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* Delivery is at least once: a publish that is retried may reach a handler twice, so every handler is idempotent
|
|
875
|
+
* (projections overwrite their documents, side effects use `withIdempotency`).
|
|
876
|
+
*/
|
|
721
877
|
interface EventBusAdapter {
|
|
878
|
+
capabilities(): readonly string[];
|
|
879
|
+
/**
|
|
880
|
+
* Resolves only after every matching subscription this instance holds has handled the event, so a read straight
|
|
881
|
+
* after a command sees its projections. A handler that throws is logged, and does not reject the publish.
|
|
882
|
+
*/
|
|
722
883
|
publish<T>(event: NymbalEvent<T>): Promise<void>;
|
|
723
884
|
publishBatch(events: NymbalEvent<unknown>[]): Promise<void>;
|
|
724
885
|
/** Delivers to `derived` subscriptions only, so rebuilding derived state never triggers side effects. */
|
|
@@ -726,80 +887,24 @@ interface EventBusAdapter {
|
|
|
726
887
|
subscribe(eventPattern: string | string[], handler: EventHandler, config?: SubscriptionConfig): Promise<Subscription>;
|
|
727
888
|
unsubscribe(subscription: Subscription): Promise<void>;
|
|
728
889
|
close(): Promise<void>;
|
|
890
|
+
/**
|
|
891
|
+
* Whether the bus can deliver, within `HEALTH_CHECK_TIMEOUT_MS`. Optional, so a bus written before it existed still
|
|
892
|
+
* type-checks; `nymbal doctor` reports one without it as `no healthCheck`.
|
|
893
|
+
*/
|
|
894
|
+
healthCheck?(): Promise<HealthReport>;
|
|
729
895
|
}
|
|
730
896
|
|
|
731
|
-
|
|
732
|
-
interface
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
error(obj: unknown, msg?: string): void;
|
|
743
|
-
child(bindings: Record<string, unknown>): Logger;
|
|
744
|
-
}
|
|
745
|
-
interface RequestContext<TBody = unknown, TQuery = Record<string, string | string[] | undefined>, TParams = Record<string, string>> {
|
|
746
|
-
method: HttpMethod;
|
|
747
|
-
path: string;
|
|
748
|
-
url: string;
|
|
749
|
-
params: TParams;
|
|
750
|
-
query: TQuery;
|
|
751
|
-
body: TBody;
|
|
752
|
-
rawBody?: Buffer;
|
|
753
|
-
headers: Record<string, string | string[] | undefined>;
|
|
754
|
-
cookies: Record<string, string>;
|
|
755
|
-
auth?: RequestAuth;
|
|
756
|
-
requestId: string;
|
|
757
|
-
correlationId: string;
|
|
758
|
-
logger: Logger;
|
|
759
|
-
ip: string;
|
|
760
|
-
}
|
|
761
|
-
interface ResponseCookie {
|
|
762
|
-
name: string;
|
|
763
|
-
value: string;
|
|
764
|
-
path?: string;
|
|
765
|
-
domain?: string;
|
|
766
|
-
maxAge?: number;
|
|
767
|
-
httpOnly?: boolean;
|
|
768
|
-
secure?: boolean;
|
|
769
|
-
sameSite?: 'strict' | 'lax' | 'none';
|
|
770
|
-
}
|
|
771
|
-
interface ResponseEnvelope<T = unknown> {
|
|
772
|
-
status: number;
|
|
773
|
-
headers?: Record<string, string>;
|
|
774
|
-
body?: T;
|
|
775
|
-
cookies?: ResponseCookie[];
|
|
776
|
-
}
|
|
777
|
-
type RouteHandler<TBody = unknown, TQuery = Record<string, string | string[] | undefined>, TParams = Record<string, string>, TResponse = unknown> = (ctx: RequestContext<TBody, TQuery, TParams>) => Promise<ResponseEnvelope<TResponse>> | ResponseEnvelope<TResponse>;
|
|
778
|
-
interface RouteOptions {
|
|
779
|
-
name?: string;
|
|
780
|
-
csrf?: boolean;
|
|
781
|
-
rateLimit?: string;
|
|
782
|
-
rawBody?: boolean;
|
|
783
|
-
}
|
|
784
|
-
type MiddlewareHandler = (ctx: RequestContext) => Promise<ResponseEnvelope | void> | ResponseEnvelope | void;
|
|
785
|
-
interface SseEvent {
|
|
786
|
-
id?: string;
|
|
787
|
-
event?: string;
|
|
788
|
-
data: unknown;
|
|
789
|
-
}
|
|
790
|
-
interface StreamContext {
|
|
791
|
-
send(event: SseEvent): void;
|
|
792
|
-
close(): void;
|
|
793
|
-
onClose(callback: () => void): void;
|
|
794
|
-
}
|
|
795
|
-
type StreamHandler = (ctx: RequestContext, stream: StreamContext) => Promise<void> | void;
|
|
796
|
-
interface HttpAdapter {
|
|
797
|
-
registerRoute(method: HttpMethod, path: string, handler: RouteHandler, options?: RouteOptions): void;
|
|
798
|
-
registerStreamRoute(method: 'GET', path: string, handler: StreamHandler, options?: RouteOptions): void;
|
|
799
|
-
registerMiddleware(middleware: MiddlewareHandler): void;
|
|
800
|
-
setDefaultHeaders(headers: Record<string, string>): void;
|
|
801
|
-
start(port: number, host?: string): Promise<void>;
|
|
802
|
-
stop(): Promise<void>;
|
|
897
|
+
/** Where uploaded and imported media go. `put` returns the URL the stored bytes are served from. */
|
|
898
|
+
interface MediaStorage {
|
|
899
|
+
capabilities(): readonly string[];
|
|
900
|
+
put(key: string, body: Uint8Array, contentType: string): Promise<{
|
|
901
|
+
url: string;
|
|
902
|
+
}>;
|
|
903
|
+
/**
|
|
904
|
+
* Whether `put` can store, checked without storing anything, within `HEALTH_CHECK_TIMEOUT_MS`. Optional, so a
|
|
905
|
+
* storage written before it existed still type-checks; `nymbal doctor` reports one without it as `no healthCheck`.
|
|
906
|
+
*/
|
|
907
|
+
healthCheck?(): Promise<HealthReport>;
|
|
803
908
|
}
|
|
804
909
|
|
|
805
910
|
type RateLimitAction = 'block' | 'challenge' | 'throttle';
|
|
@@ -860,8 +965,18 @@ interface RefundOptions {
|
|
|
860
965
|
/** The order's currency: what a provider that no longer holds the payment answers in. */
|
|
861
966
|
currency?: string;
|
|
862
967
|
}
|
|
968
|
+
/**
|
|
969
|
+
* The storefront half of a payments adapter: which client-side driver collects the payment and the public values it
|
|
970
|
+
* needs. GET /api/capabilities serves `options` to every shopper, so it holds public values only.
|
|
971
|
+
*/
|
|
972
|
+
interface PaymentClient {
|
|
973
|
+
driver: string;
|
|
974
|
+
options: Record<string, string>;
|
|
975
|
+
}
|
|
863
976
|
interface PaymentsAdapter extends CommerceAdapter {
|
|
864
977
|
readonly kind: 'payments';
|
|
978
|
+
/** How a storefront collects a payment; absent or `null` when there is no client step. */
|
|
979
|
+
client?(): PaymentClient | null;
|
|
865
980
|
createPaymentIntent(params: PaymentIntentParams): Promise<PaymentIntent>;
|
|
866
981
|
capturePayment(intentId: string, amountMinor?: number): Promise<PaymentCapture>;
|
|
867
982
|
/** Throws `PaymentError('refund_rejected')` when no money moved, `PaymentError('refund_unknown')` when it may have. */
|
|
@@ -941,6 +1056,8 @@ interface SearchResult {
|
|
|
941
1056
|
nextCursor: string | null;
|
|
942
1057
|
}
|
|
943
1058
|
interface FacetParams {
|
|
1059
|
+
/** Count only the documents this query matches. */
|
|
1060
|
+
query?: string;
|
|
944
1061
|
limit?: number;
|
|
945
1062
|
filters?: Record<string, unknown>;
|
|
946
1063
|
}
|
|
@@ -1026,5 +1143,81 @@ interface AiAdapter extends CommerceAdapter {
|
|
|
1026
1143
|
maxTokens?: number;
|
|
1027
1144
|
}): Promise<string>;
|
|
1028
1145
|
}
|
|
1146
|
+
/** The adapter interface each commerce slot takes: what a project injects for a slot whose provider is `custom`. */
|
|
1147
|
+
interface CommerceSlotAdapters {
|
|
1148
|
+
payments: PaymentsAdapter;
|
|
1149
|
+
email: EmailAdapter;
|
|
1150
|
+
reviews: ReviewsAdapter;
|
|
1151
|
+
search: SearchAdapter;
|
|
1152
|
+
analytics: AnalyticsAdapter;
|
|
1153
|
+
shipping: ShippingAdapter;
|
|
1154
|
+
tax: TaxAdapter;
|
|
1155
|
+
ai: AiAdapter;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
interface RenderedEmail {
|
|
1159
|
+
subject: string;
|
|
1160
|
+
text: string;
|
|
1161
|
+
html: string;
|
|
1162
|
+
}
|
|
1163
|
+
declare function renderEmailTemplate(template: string, vars: Record<string, unknown>): RenderedEmail;
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* What GET /api/capabilities tells a storefront about the store: what its adapters can do, never which vendor they
|
|
1167
|
+
* are. It names no provider and holds no secret.
|
|
1168
|
+
*/
|
|
1169
|
+
interface StoreCapabilities {
|
|
1170
|
+
payments: {
|
|
1171
|
+
capabilities: string[];
|
|
1172
|
+
client: PaymentClient | null;
|
|
1173
|
+
};
|
|
1174
|
+
search: {
|
|
1175
|
+
capabilities: string[];
|
|
1176
|
+
};
|
|
1177
|
+
/** The listing orders GET /api/products accepts, in display order, and the one it uses when none is asked. */
|
|
1178
|
+
catalog: {
|
|
1179
|
+
sortKeys: ProductSortKey[];
|
|
1180
|
+
defaultSort: ProductSortKey;
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* A value a recipe writes into `nymbal.config.ts`. `{ env }` is written as `env('<name>')`, or
|
|
1186
|
+
* `env('<name>', '<default>')` when a default is given, so no secret ever lands in the file.
|
|
1187
|
+
*/
|
|
1188
|
+
type RecipeValue = string | boolean | number | {
|
|
1189
|
+
env: string;
|
|
1190
|
+
default?: string;
|
|
1191
|
+
} | {
|
|
1192
|
+
[k: string]: RecipeValue;
|
|
1193
|
+
};
|
|
1194
|
+
/**
|
|
1195
|
+
* What `nymbal add <name>` does to a project: data shipped as `recipes/<name>.json` inside the package it belongs to.
|
|
1196
|
+
*/
|
|
1197
|
+
interface Recipe {
|
|
1198
|
+
name: string;
|
|
1199
|
+
summary: string;
|
|
1200
|
+
/** npm packages to install, SDKs included; empty when the service is built in. */
|
|
1201
|
+
install: string[];
|
|
1202
|
+
/** Config paths and the values they take, e.g. `infrastructure.documentStore` = `'redis'`. */
|
|
1203
|
+
set: Array<{
|
|
1204
|
+
path: string;
|
|
1205
|
+
value: RecipeValue;
|
|
1206
|
+
}>;
|
|
1207
|
+
/** The variables the service reads, appended to `.env.example`; an optional one is written commented out. */
|
|
1208
|
+
env: Array<{
|
|
1209
|
+
name: string;
|
|
1210
|
+
example: string;
|
|
1211
|
+
required: boolean;
|
|
1212
|
+
comment: string;
|
|
1213
|
+
}>;
|
|
1214
|
+
/** Lines printed after a successful add. */
|
|
1215
|
+
next: string[];
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
type StockLevel = 'out' | 'low' | 'in';
|
|
1219
|
+
/** The one stock rule every storefront renders from. */
|
|
1220
|
+
declare function stockLevel(stock: number): StockLevel;
|
|
1221
|
+
declare const STOCK_LABELS: Record<StockLevel, string>;
|
|
1029
1222
|
|
|
1030
|
-
export { type AdapterBase, AdapterError, type AdapterErrorV1Payload, type Address, type AggregateRating, type AiAdapter, type AnalyticsAdapter, AuthError, type CaptureMethod, type Cart, type CartAbandonedV1Payload, type CartConvertedV1Payload, type CartCreatedV1Payload, type CartItem, type CartItemAddedV1Payload, type CartItemRemovedV1Payload, type CartUpdatedV1Payload, type Category, type CategoryCreatedV1Payload, type CategoryDeletedV1Payload, type CategoryUpdatedV1Payload, CheckoutError, type CheckoutPayment, type CommerceAdapter, ConfigError, type Customer, type CustomerAddress, type CustomerCreatedV1Payload, type CustomerDeletedV1Payload, type CustomerProfile, type CustomerUpdatedV1Payload, DEFAULT_PRODUCT_SORT, type DocumentStoreAdapter, type DriftReport, EVT_ADAPTER_ERROR, EVT_CART_ABANDONED, EVT_CART_CONVERTED, EVT_CART_CREATED, EVT_CART_ITEM_ADDED, EVT_CART_ITEM_REMOVED, EVT_CART_UPDATED, EVT_CATEGORY_CREATED, EVT_CATEGORY_DELETED, EVT_CATEGORY_UPDATED, EVT_CUSTOMER_CREATED, EVT_CUSTOMER_DELETED, EVT_CUSTOMER_REGISTERED, EVT_CUSTOMER_UPDATED, EVT_INVENTORY_CHANGED, EVT_INVENTORY_LOW_STOCK, EVT_INVENTORY_OUT_OF_STOCK, EVT_INVENTORY_RELEASED, EVT_INVENTORY_RESERVED, EVT_ORDER_CANCELLED, EVT_ORDER_DELIVERED, EVT_ORDER_FULFILLED, EVT_ORDER_PAID, EVT_ORDER_PARTIALLY_REFUNDED, EVT_ORDER_PLACED, EVT_ORDER_REFUNDED, EVT_ORDER_SHIPPED, EVT_PAYMENT_CAPTURED, EVT_PAYMENT_FAILED, EVT_PAYMENT_REFUNDED, EVT_PRODUCT_CREATED, EVT_PRODUCT_DELETED, EVT_PRODUCT_PUBLISHED, EVT_PRODUCT_UNPUBLISHED, EVT_PRODUCT_UPDATED, type EmailAdapter, type EmailSendResult, type ErrorContext, type EventBusAdapter, type EventHandler, type EventMetadata, type Facet, type FacetParams, type HealthReport, type HealthStatus, type HttpAdapter, HttpError, type HttpMethod, type IndexResult, type InventoryChangedV1Payload, type InventoryLowStockV1Payload, type InventoryOutOfStockV1Payload, type InventoryReleasedV1Payload, type InventoryReservation, type InventoryReservedV1Payload, type Logger, type MiddlewareHandler, type Money, NotFoundError, type NymbalEnvironment, NymbalError, type NymbalEvent, type NymbalEventMap, type NymbalEventType, ORDER_STATUS_TRANSITIONS, type Order, type OrderCancelledV1Payload, OrderConflictError, type OrderDeliveredV1Payload, type OrderFulfilledV1Payload, type OrderHistoryEntry, type OrderLineItem, type OrderPaidV1Payload, type OrderPartiallyRefundedV1Payload, type OrderPayment, type OrderPlacedV1Payload, type OrderRefund, type OrderRefundStatus, type OrderRefundedV1Payload, type OrderShippedV1Payload, type OrderStatus, PRODUCT_SORT_KEYS, type PaginationParams, type PayloadOf, type PaymentCapture, type PaymentCapturedV1Payload, PaymentError, type PaymentFailedV1Payload, type PaymentIntent, type PaymentIntentParams, type PaymentRefundedV1Payload, type PaymentState, type PaymentStatus, type PaymentsAdapter, type Product, type ProductCreatedV1Payload, type ProductDeletedV1Payload, type ProductMedia, type ProductPublishedV1Payload, type ProductSnapshot, type ProductSortKey, type ProductStatus, type ProductType, type ProductUnpublishedV1Payload, type ProductUpdatedV1Payload, type PutOptions, type QueryParams, type QueryResult, type RateLimitAction, type RateLimitDecision, type RateLimitRule, type Refund, type RefundOptions, type RequestAuth, type RequestContext, type ResponseCookie, type ResponseEnvelope, type RetryPolicy, type Review, type ReviewModerationStatus, type ReviewResult, type ReviewSubmission, type ReviewsAdapter, type ReviewsResult, type Role, type RouteHandler, type RouteOptions, STANDARD_EVENT_TYPES, type SearchAdapter, type SearchDocument, type SearchParams, type SearchResult, type SecurityAdapter, type SecurityConfigLike, type SecurityState, type ShippingAdapter, type ShippingLabel, type ShippingLabelParams, type ShippingRate, type ShippingRateParams, type ShippingTracking, type SseEvent, StatusTransitionError, type StockAdjustment, type StockAdjustmentReason, type StockStatus, type StreamContext, type StreamHandler, type Subscription, type SubscriptionConfig, type TaxAdapter, type TaxCalculationParams, type TaxCalculationResult, type TaxLine, type TransactionalEmail, ValidationError, type ValidationResult, type Variant, type VariantDimensions, type VariantOption, type VariantStatus, type WebhookResult, add, allocate, assertMinorUnits, assertOrderTransition, canTransitionOrder, money, mulBy, paymentState, refundableMinor, sub, sumMoney };
|
|
1223
|
+
export { type AdapterBase, type AdapterContext, AdapterError, type AdapterErrorV1Payload, type Address, type AggregateRating, type AiAdapter, type AnalyticsAdapter, AuthError, type BoundedHealthCheckOptions, type CaptureMethod, type Cart, type CartAbandonedV1Payload, type CartConvertedV1Payload, type CartCreatedV1Payload, type CartItem, type CartItemAddedV1Payload, type CartItemRemovedV1Payload, type CartUpdatedV1Payload, type Category, type CategoryCreatedV1Payload, type CategoryDeletedV1Payload, type CategoryUpdatedV1Payload, CheckoutError, type CheckoutPayment, type CommerceAdapter, type CommerceSlot, type CommerceSlotAdapters, ConfigError, type Customer, type CustomerAddress, type CustomerCreatedV1Payload, type CustomerDeletedV1Payload, type CustomerProfile, type CustomerUpdatedV1Payload, DEFAULT_PRODUCT_SORT, DEFAULT_QUERY_LIMIT, type DocumentStoreAdapter, type DriftReport, EVT_ADAPTER_ERROR, EVT_CART_ABANDONED, EVT_CART_CONVERTED, EVT_CART_CREATED, EVT_CART_ITEM_ADDED, EVT_CART_ITEM_REMOVED, EVT_CART_UPDATED, EVT_CATEGORY_CREATED, EVT_CATEGORY_DELETED, EVT_CATEGORY_UPDATED, EVT_CUSTOMER_CREATED, EVT_CUSTOMER_DELETED, EVT_CUSTOMER_REGISTERED, EVT_CUSTOMER_UPDATED, EVT_INVENTORY_CHANGED, EVT_INVENTORY_LOW_STOCK, EVT_INVENTORY_OUT_OF_STOCK, EVT_INVENTORY_RELEASED, EVT_INVENTORY_RESERVED, EVT_ORDER_CANCELLED, EVT_ORDER_DELIVERED, EVT_ORDER_FULFILLED, EVT_ORDER_PAID, EVT_ORDER_PARTIALLY_REFUNDED, EVT_ORDER_PLACED, EVT_ORDER_REFUNDED, EVT_ORDER_SHIPPED, EVT_PAYMENT_CAPTURED, EVT_PAYMENT_FAILED, EVT_PAYMENT_REFUNDED, EVT_PRODUCT_CREATED, EVT_PRODUCT_DELETED, EVT_PRODUCT_PUBLISHED, EVT_PRODUCT_UNPUBLISHED, EVT_PRODUCT_UPDATED, type EmailAdapter, type EmailSendResult, type ErrorContext, type EventBusAdapter, type EventHandler, type EventMetadata, type Facet, type FacetParams, HEALTH_CHECK_TIMEOUT_MS, type HealthReport, type HealthStatus, type HttpAdapter, HttpError, type HttpMethod, type IndexResult, type InventoryChangedV1Payload, type InventoryLowStockV1Payload, type InventoryOutOfStockV1Payload, type InventoryReleasedV1Payload, type InventoryReservation, type InventoryReservedV1Payload, type Logger, type MediaStorage, type MiddlewareHandler, type Money, NotFoundError, type NymbalEnvironment, NymbalError, type NymbalEvent, type NymbalEventMap, type NymbalEventType, ORDER_STATUS_TRANSITIONS, type Order, type OrderCancelledV1Payload, OrderConflictError, type OrderDeliveredV1Payload, type OrderFulfilledV1Payload, type OrderHistoryEntry, type OrderLineItem, type OrderPaidV1Payload, type OrderPartiallyRefundedV1Payload, type OrderPayment, type OrderPlacedV1Payload, type OrderRefund, type OrderRefundStatus, type OrderRefundedV1Payload, type OrderShippedV1Payload, type OrderStatus, PRODUCT_SORT_KEYS, PRODUCT_SORT_LABELS, type PaginationParams, type PayloadOf, type PaymentCapture, type PaymentCapturedV1Payload, type PaymentClient, PaymentError, type PaymentFailedV1Payload, type PaymentIntent, type PaymentIntentParams, type PaymentRefundedV1Payload, type PaymentState, type PaymentStatus, type PaymentsAdapter, type Product, type ProductCreatedV1Payload, type ProductDeletedV1Payload, type ProductMedia, type ProductPublishedV1Payload, type ProductSnapshot, type ProductSortKey, type ProductStatus, type ProductType, type ProductUnpublishedV1Payload, type ProductUpdatedV1Payload, type PutOptions, type QueryParams, type QueryResult, type RateLimitAction, type RateLimitDecision, type RateLimitRule, type Recipe, type RecipeValue, type Refund, type RefundOptions, type RenderedEmail, type RequestAuth, type RequestContext, type ResponseCookie, type ResponseEnvelope, type RetryPolicy, type Review, type ReviewModerationStatus, type ReviewResult, type ReviewSubmission, type ReviewsAdapter, type ReviewsResult, type Role, type RouteHandler, type RouteOptions, SECRET_MIN_LENGTH, STANDARD_EVENT_TYPES, STOCK_LABELS, type SearchAdapter, type SearchDocument, type SearchParams, type SearchResult, type SecurityAdapter, type SecurityConfigLike, type SecurityState, type ShippingAdapter, type ShippingLabel, type ShippingLabelParams, type ShippingRate, type ShippingRateParams, type ShippingTracking, type Slot, type SortKeyCondition, type SseEvent, StatusTransitionError, type StockAdjustment, type StockAdjustmentReason, type StockLevel, type StockStatus, type StoreCapabilities, type StreamContext, type StreamHandler, type Subscription, type SubscriptionConfig, type TaxAdapter, type TaxCalculationParams, type TaxCalculationResult, type TaxLine, type TransactionalEmail, ValidationError, type ValidationResult, type Variant, type VariantDimensions, type VariantOption, type VariantStatus, type WebhookResult, add, allocate, assertMinorUnits, assertOrderTransition, boundedHealthCheck, canTransitionOrder, compareDocumentValues, connectionSecrets, displayUrl, documentValueAt, errorText, matchesSortKey, money, mulBy, paymentState, queryDocuments, refundableMinor, renderEmailTemplate, stockLevel, sub, sumMoney };
|
package/dist/index.js
CHANGED
|
@@ -142,6 +142,13 @@ var PRODUCT_SORT_KEYS = [
|
|
|
142
142
|
"name-desc"
|
|
143
143
|
];
|
|
144
144
|
var DEFAULT_PRODUCT_SORT = "newest";
|
|
145
|
+
var PRODUCT_SORT_LABELS = {
|
|
146
|
+
newest: "Newest",
|
|
147
|
+
"price-asc": "Price: Low to High",
|
|
148
|
+
"price-desc": "Price: High to Low",
|
|
149
|
+
"name-asc": "Name: A to Z",
|
|
150
|
+
"name-desc": "Name: Z to A"
|
|
151
|
+
};
|
|
145
152
|
|
|
146
153
|
// src/domain/order.ts
|
|
147
154
|
var ORDER_STATUS_TRANSITIONS = {
|
|
@@ -244,12 +251,275 @@ var STANDARD_EVENT_TYPES = [
|
|
|
244
251
|
EVT_PAYMENT_REFUNDED,
|
|
245
252
|
EVT_ADAPTER_ERROR
|
|
246
253
|
];
|
|
254
|
+
|
|
255
|
+
// src/adapters/adapter-base.ts
|
|
256
|
+
var HEALTH_CHECK_TIMEOUT_MS = 5e3;
|
|
257
|
+
function errorText(error) {
|
|
258
|
+
if (!(error instanceof Error)) return String(error);
|
|
259
|
+
if (error.message !== "") return error.message;
|
|
260
|
+
const inner = error.errors;
|
|
261
|
+
if (Array.isArray(inner) && inner.length > 0) return inner.map(errorText).join("; ");
|
|
262
|
+
const code = error.code;
|
|
263
|
+
return typeof code === "string" ? code : error.name;
|
|
264
|
+
}
|
|
265
|
+
var SECRET_MIN_LENGTH = 8;
|
|
266
|
+
function connectionSecrets(url, options = {}) {
|
|
267
|
+
let parsed;
|
|
268
|
+
try {
|
|
269
|
+
parsed = new URL(url);
|
|
270
|
+
} catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const raw = [
|
|
274
|
+
...options.user === false ? [] : [parsed.username],
|
|
275
|
+
parsed.password,
|
|
276
|
+
...parsed.searchParams.values()
|
|
277
|
+
];
|
|
278
|
+
const decoded = raw.map((value) => {
|
|
279
|
+
try {
|
|
280
|
+
return decodeURIComponent(value);
|
|
281
|
+
} catch {
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return [.../* @__PURE__ */ new Set([...raw, ...decoded])].filter((value) => value.length >= SECRET_MIN_LENGTH);
|
|
286
|
+
}
|
|
287
|
+
function displayUrl(url) {
|
|
288
|
+
try {
|
|
289
|
+
const parsed = new URL(url);
|
|
290
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname === "/" ? "" : parsed.pathname}`;
|
|
291
|
+
} catch {
|
|
292
|
+
return "<unparsable url>";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function boundedHealthCheck(check, options = {}) {
|
|
296
|
+
const timeoutMs = options.timeoutMs ?? HEALTH_CHECK_TIMEOUT_MS;
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
let timer;
|
|
299
|
+
const timedOut = new Promise((resolve) => {
|
|
300
|
+
timer = setTimeout(() => {
|
|
301
|
+
controller.abort();
|
|
302
|
+
resolve({ status: "down", detail: `timed out after ${timeoutMs} ms` });
|
|
303
|
+
}, timeoutMs);
|
|
304
|
+
});
|
|
305
|
+
const failed = (error) => {
|
|
306
|
+
let detail = errorText(error);
|
|
307
|
+
for (const secret of options.redact ?? []) {
|
|
308
|
+
if (secret !== "") detail = detail.split(secret).join("***");
|
|
309
|
+
}
|
|
310
|
+
return { status: "down", detail };
|
|
311
|
+
};
|
|
312
|
+
try {
|
|
313
|
+
const checked = Promise.resolve().then(() => check(controller.signal)).catch(failed);
|
|
314
|
+
return await Promise.race([checked, timedOut]);
|
|
315
|
+
} finally {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/adapters/document-order.ts
|
|
321
|
+
var collator = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
322
|
+
function rankOf(value) {
|
|
323
|
+
if (typeof value === "number") return 0;
|
|
324
|
+
if (typeof value === "string") return 1;
|
|
325
|
+
if (value === null || value === void 0) return 3;
|
|
326
|
+
return 2;
|
|
327
|
+
}
|
|
328
|
+
function sign(n) {
|
|
329
|
+
return n > 0 ? 1 : n < 0 ? -1 : 0;
|
|
330
|
+
}
|
|
331
|
+
function compareOther(a, b) {
|
|
332
|
+
const ta = typeof a;
|
|
333
|
+
const tb = typeof b;
|
|
334
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
335
|
+
if (ta === "boolean") return Number(a) - Number(b);
|
|
336
|
+
const ja = JSON.stringify(a) ?? "";
|
|
337
|
+
const jb = JSON.stringify(b) ?? "";
|
|
338
|
+
return ja < jb ? -1 : ja > jb ? 1 : 0;
|
|
339
|
+
}
|
|
340
|
+
function compareDocumentValues(a, b) {
|
|
341
|
+
const ra = rankOf(a);
|
|
342
|
+
const rb = rankOf(b);
|
|
343
|
+
if (ra !== rb) return ra - rb;
|
|
344
|
+
switch (ra) {
|
|
345
|
+
case 0:
|
|
346
|
+
return sign(a - b);
|
|
347
|
+
case 1:
|
|
348
|
+
return sign(collator.compare(a, b));
|
|
349
|
+
case 2:
|
|
350
|
+
return sign(compareOther(a, b));
|
|
351
|
+
default:
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function matchesSortKey(value, condition) {
|
|
356
|
+
const target = condition.value;
|
|
357
|
+
switch (condition.operator) {
|
|
358
|
+
case "eq":
|
|
359
|
+
return value === target;
|
|
360
|
+
case "gt":
|
|
361
|
+
return compareDocumentValues(value, target) > 0;
|
|
362
|
+
case "lt":
|
|
363
|
+
return compareDocumentValues(value, target) < 0;
|
|
364
|
+
case "between": {
|
|
365
|
+
if (!Array.isArray(target) || target.length !== 2) return false;
|
|
366
|
+
const [low, high] = target;
|
|
367
|
+
return compareDocumentValues(value, low) >= 0 && compareDocumentValues(value, high) <= 0;
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function documentValueAt(doc, path) {
|
|
374
|
+
let value = doc;
|
|
375
|
+
for (const part of path.split(".")) {
|
|
376
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
377
|
+
value = value[part];
|
|
378
|
+
}
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
var DEFAULT_QUERY_LIMIT = 50;
|
|
382
|
+
function orderOf(params) {
|
|
383
|
+
if (params.orderBy !== void 0) {
|
|
384
|
+
return { field: params.orderBy.field, direction: params.orderBy.direction };
|
|
385
|
+
}
|
|
386
|
+
const direction = params.ascending ?? true ? "asc" : "desc";
|
|
387
|
+
return { field: params.sortKey?.field ?? null, direction };
|
|
388
|
+
}
|
|
389
|
+
var isNullish = (value) => value === null || value === void 0;
|
|
390
|
+
function comparePositions(a, b, order) {
|
|
391
|
+
if (order.field !== null) {
|
|
392
|
+
const an = isNullish(a.value);
|
|
393
|
+
const bn = isNullish(b.value);
|
|
394
|
+
if (an !== bn) return an ? 1 : -1;
|
|
395
|
+
let c2 = an ? 0 : compareDocumentValues(a.value, b.value);
|
|
396
|
+
if (order.direction === "desc") c2 = -c2;
|
|
397
|
+
if (c2 !== 0) return c2;
|
|
398
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
399
|
+
}
|
|
400
|
+
const c = a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
401
|
+
return order.direction === "desc" ? -c : c;
|
|
402
|
+
}
|
|
403
|
+
function cursorScope(order, params) {
|
|
404
|
+
return JSON.stringify([order.field, order.direction, params.partitionKey]);
|
|
405
|
+
}
|
|
406
|
+
function decodeCursor(cursor, scope) {
|
|
407
|
+
let decoded;
|
|
408
|
+
try {
|
|
409
|
+
decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
410
|
+
} catch {
|
|
411
|
+
decoded = null;
|
|
412
|
+
}
|
|
413
|
+
const c = decoded;
|
|
414
|
+
if (c === null || typeof c !== "object" || typeof c.o !== "string" || typeof c.k !== "string") {
|
|
415
|
+
throw new ValidationError("The cursor is not valid", { context: { cursor } });
|
|
416
|
+
}
|
|
417
|
+
if (c.o !== scope) {
|
|
418
|
+
throw new ValidationError("The cursor was issued under another order", { context: { cursor } });
|
|
419
|
+
}
|
|
420
|
+
return { key: c.k, value: c.v ?? null };
|
|
421
|
+
}
|
|
422
|
+
function queryDocuments(documents, params) {
|
|
423
|
+
const limit = params.limit ?? DEFAULT_QUERY_LIMIT;
|
|
424
|
+
const order = orderOf(params);
|
|
425
|
+
const scope = cursorScope(order, params);
|
|
426
|
+
const after = params.cursor ? decodeCursor(params.cursor, scope) : null;
|
|
427
|
+
const { partitionKey, sortKey } = params;
|
|
428
|
+
const matches = [];
|
|
429
|
+
for (const [key, doc] of documents) {
|
|
430
|
+
const record = doc ?? {};
|
|
431
|
+
if (partitionKey.value !== "*" && record[partitionKey.field] !== partitionKey.value) continue;
|
|
432
|
+
if (sortKey !== void 0 && !matchesSortKey(record[sortKey.field], sortKey)) continue;
|
|
433
|
+
const value = order.field === null ? null : documentValueAt(record, order.field) ?? null;
|
|
434
|
+
const position = { key, value };
|
|
435
|
+
if (after !== null && comparePositions(position, after, order) <= 0) continue;
|
|
436
|
+
matches.push({ position, doc });
|
|
437
|
+
}
|
|
438
|
+
matches.sort((a, b) => comparePositions(a.position, b.position, order));
|
|
439
|
+
const page = matches.slice(0, limit);
|
|
440
|
+
const last = page[page.length - 1];
|
|
441
|
+
const nextCursor = matches.length > limit && last !== void 0 ? Buffer.from(
|
|
442
|
+
JSON.stringify({ o: scope, k: last.position.key, v: last.position.value }),
|
|
443
|
+
"utf8"
|
|
444
|
+
).toString("base64url") : null;
|
|
445
|
+
return { items: page.map((m) => m.doc), nextCursor };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/adapters/email-templates.ts
|
|
449
|
+
function render(template, vars) {
|
|
450
|
+
return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
|
|
451
|
+
const parts = path.split(".");
|
|
452
|
+
let cur = vars;
|
|
453
|
+
for (const p of parts) {
|
|
454
|
+
if (cur && typeof cur === "object" && p in cur) {
|
|
455
|
+
cur = cur[p];
|
|
456
|
+
} else {
|
|
457
|
+
return "";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return cur == null ? "" : String(cur);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
var TEMPLATES = {
|
|
464
|
+
"order-placed": {
|
|
465
|
+
subject: "Order {{orderNumber}} confirmed",
|
|
466
|
+
body: `Hi {{customerName}},
|
|
467
|
+
|
|
468
|
+
Thank you for your order {{orderNumber}}. Total: {{totalFormatted}}.
|
|
469
|
+
|
|
470
|
+
We'll let you know when it ships.`
|
|
471
|
+
},
|
|
472
|
+
"order-shipped": {
|
|
473
|
+
subject: "Order {{orderNumber}} is on its way",
|
|
474
|
+
body: `Hi {{customerName}},
|
|
475
|
+
|
|
476
|
+
Your order {{orderNumber}} has shipped via {{carrier}}.
|
|
477
|
+
Tracking: {{trackingNumber}}`
|
|
478
|
+
},
|
|
479
|
+
welcome: {
|
|
480
|
+
subject: "Welcome to {{storeName}}",
|
|
481
|
+
body: `Welcome {{customerName}}! Your account is ready.`
|
|
482
|
+
},
|
|
483
|
+
"cart-abandoned": {
|
|
484
|
+
subject: "Don't forget your cart at {{storeName}}",
|
|
485
|
+
body: `Hi,
|
|
486
|
+
|
|
487
|
+
You left items in your cart. Pick up where you left off: {{resumeUrl}}`
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
function renderEmailTemplate(template, vars) {
|
|
491
|
+
const t = TEMPLATES[template];
|
|
492
|
+
if (!t) {
|
|
493
|
+
return {
|
|
494
|
+
subject: `[nymbal] ${template}`,
|
|
495
|
+
text: JSON.stringify(vars),
|
|
496
|
+
html: `<pre>${JSON.stringify(vars, null, 2)}</pre>`
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const subject = render(t.subject, vars);
|
|
500
|
+
const text = render(t.body, vars);
|
|
501
|
+
const html = `<pre>${text.replace(/</g, "<")}</pre>`;
|
|
502
|
+
return { subject, text, html };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/stock-level.ts
|
|
506
|
+
var LOW_STOCK_BELOW = 5;
|
|
507
|
+
function stockLevel(stock) {
|
|
508
|
+
if (stock <= 0) return "out";
|
|
509
|
+
return stock < LOW_STOCK_BELOW ? "low" : "in";
|
|
510
|
+
}
|
|
511
|
+
var STOCK_LABELS = {
|
|
512
|
+
out: "Out of Stock",
|
|
513
|
+
low: "Low Stock",
|
|
514
|
+
in: "In Stock"
|
|
515
|
+
};
|
|
247
516
|
export {
|
|
248
517
|
AdapterError,
|
|
249
518
|
AuthError,
|
|
250
519
|
CheckoutError,
|
|
251
520
|
ConfigError,
|
|
252
521
|
DEFAULT_PRODUCT_SORT,
|
|
522
|
+
DEFAULT_QUERY_LIMIT,
|
|
253
523
|
EVT_ADAPTER_ERROR,
|
|
254
524
|
EVT_CART_ABANDONED,
|
|
255
525
|
EVT_CART_CONVERTED,
|
|
@@ -285,25 +555,39 @@ export {
|
|
|
285
555
|
EVT_PRODUCT_PUBLISHED,
|
|
286
556
|
EVT_PRODUCT_UNPUBLISHED,
|
|
287
557
|
EVT_PRODUCT_UPDATED,
|
|
558
|
+
HEALTH_CHECK_TIMEOUT_MS,
|
|
288
559
|
HttpError,
|
|
289
560
|
NotFoundError,
|
|
290
561
|
NymbalError,
|
|
291
562
|
ORDER_STATUS_TRANSITIONS,
|
|
292
563
|
OrderConflictError,
|
|
293
564
|
PRODUCT_SORT_KEYS,
|
|
565
|
+
PRODUCT_SORT_LABELS,
|
|
294
566
|
PaymentError,
|
|
567
|
+
SECRET_MIN_LENGTH,
|
|
295
568
|
STANDARD_EVENT_TYPES,
|
|
569
|
+
STOCK_LABELS,
|
|
296
570
|
StatusTransitionError,
|
|
297
571
|
ValidationError,
|
|
298
572
|
add,
|
|
299
573
|
allocate,
|
|
300
574
|
assertMinorUnits,
|
|
301
575
|
assertOrderTransition,
|
|
576
|
+
boundedHealthCheck,
|
|
302
577
|
canTransitionOrder,
|
|
578
|
+
compareDocumentValues,
|
|
579
|
+
connectionSecrets,
|
|
580
|
+
displayUrl,
|
|
581
|
+
documentValueAt,
|
|
582
|
+
errorText,
|
|
583
|
+
matchesSortKey,
|
|
303
584
|
money,
|
|
304
585
|
mulBy,
|
|
305
586
|
paymentState,
|
|
587
|
+
queryDocuments,
|
|
306
588
|
refundableMinor,
|
|
589
|
+
renderEmailTemplate,
|
|
590
|
+
stockLevel,
|
|
307
591
|
sub,
|
|
308
592
|
sumMoney
|
|
309
593
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/domain/money.ts","../src/domain/product.ts","../src/domain/order.ts","../src/events/constants.ts"],"sourcesContent":["export type ErrorContext = Record<string, unknown>\n\nexport class NymbalError extends Error {\n readonly code: string\n readonly context: ErrorContext\n\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined)\n this.name = this.constructor.name\n this.code = code\n this.context = options.context ?? {}\n }\n\n toJSON(): { name: string; code: string; message: string; context: ErrorContext } {\n return { name: this.name, code: this.code, message: this.message, context: this.context }\n }\n}\n\nexport class ConfigError extends NymbalError {\n constructor(\n message: string,\n options: { cause?: unknown; context?: ErrorContext; code?: `config.${string}` } = {},\n ) {\n super(options.code ?? 'config.invalid', message, options)\n }\n}\n\nexport class ValidationError extends NymbalError {\n constructor(message: string, options: { cause?: unknown; context?: ErrorContext } = {}) {\n super('validation.failed', message, options)\n }\n}\n\nexport class NotFoundError extends NymbalError {\n constructor(\n resource: string,\n identifier: unknown,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super('not_found', `${resource} not found: ${String(identifier)}`, {\n ...options,\n context: { resource, identifier, ...(options.context ?? {}) },\n })\n }\n}\n\nexport class AdapterError extends NymbalError {\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`adapter.${code}`, message, options)\n }\n}\n\nexport class HttpError extends NymbalError {\n readonly status: number\n\n constructor(\n status: number,\n message: string,\n options: { cause?: unknown; context?: ErrorContext; code?: string } = {},\n ) {\n super(options.code ?? `http.${status}`, message, options)\n this.status = status\n }\n}\n\nexport class AuthError extends HttpError {\n constructor(\n code: 'unauthorized' | 'forbidden' | 'invalid_credentials' | 'token_expired' | 'token_invalid',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n const status = code === 'forbidden' ? 403 : 401\n super(status, message, { ...options, code: `auth.${code}` })\n }\n}\n\nexport class PaymentError extends NymbalError {\n constructor(\n code:\n | 'intent_failed'\n | 'capture_failed'\n // A definite refusal: no money moved.\n | 'refund_rejected'\n // An outcome the shop cannot know (a timeout, a provider fault): money may have moved.\n | 'refund_unknown'\n | 'cancel_refused'\n | 'webhook_invalid'\n | 'not_configured',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`payment.${code}`, message, options)\n }\n}\n\n/** A command the order's money does not allow (confirm unpaid, cancel paid, refund beyond the capture). */\nexport class OrderConflictError extends NymbalError {\n constructor(\n code:\n | 'order.unpaid'\n | 'order.paid'\n | 'order.stock_unavailable'\n | 'order.payment_in_progress'\n | 'refund.not_captured'\n | 'refund.exceeds_captured'\n | 'refund.request_reused',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(code, message, options)\n }\n}\n\nexport class CheckoutError extends NymbalError {\n constructor(\n code:\n | 'cart_empty'\n | 'in_progress'\n | 'payment_in_progress'\n | 'variant_missing'\n | 'out_of_stock'\n | 'reservation_failed'\n | 'finalize_failed',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`checkout.${code}`, message, options)\n }\n}\n\nexport class StatusTransitionError extends NymbalError {\n constructor(\n entity: string,\n fromStatus: string,\n toStatus: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(\n 'status.invalid_transition',\n `Invalid ${entity} status transition: ${fromStatus} → ${toStatus}`,\n {\n ...options,\n context: { entity, fromStatus, toStatus, ...(options.context ?? {}) },\n },\n )\n }\n}\n","import { ValidationError } from '../errors.js'\n\nexport interface Money {\n amount: number\n currency: string\n}\n\nexport function money(amount: number, currency: string): Money {\n assertMinorUnits(amount)\n return { amount, currency }\n}\n\nexport function assertMinorUnits(amount: number): void {\n if (!Number.isInteger(amount)) {\n throw new ValidationError(`Monetary amount must be an integer (minor units), got ${amount}`)\n }\n}\n\nfunction assertSameCurrency(a: Money, b: Money): void {\n if (a.currency !== b.currency) {\n throw new ValidationError(\n `Currency mismatch: ${a.currency} vs ${b.currency}`,\n )\n }\n}\n\nexport function add(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount + b.amount, currency: a.currency }\n}\n\nexport function sub(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount - b.amount, currency: a.currency }\n}\n\nexport function mulBy(m: Money, scalar: number): Money {\n if (!Number.isFinite(scalar)) {\n throw new ValidationError(`Scalar must be finite, got ${scalar}`)\n }\n return { amount: Math.round(m.amount * scalar), currency: m.currency }\n}\n\nexport function sumMoney(values: Money[], currency: string): Money {\n const total = values.reduce((acc, m) => {\n assertSameCurrency(m, { amount: 0, currency })\n return acc + m.amount\n }, 0)\n return { amount: total, currency }\n}\n\nexport function allocate(m: Money, weights: number[]): Money[] {\n if (weights.length === 0) return []\n const totalWeight = weights.reduce((s, w) => s + w, 0)\n if (totalWeight <= 0) {\n throw new ValidationError('allocate: sum of weights must be > 0')\n }\n const portions = weights.map((w) => Math.floor((m.amount * w) / totalWeight))\n let remainder = m.amount - portions.reduce((s, p) => s + p, 0)\n const result = portions.slice()\n let idx = 0\n while (remainder !== 0 && idx < result.length) {\n const delta = remainder > 0 ? 1 : -1\n result[idx] = (result[idx] ?? 0) + delta\n remainder -= delta\n idx += 1\n }\n return result.map((amount) => ({ amount, currency: m.currency }))\n}\n","export type ProductStatus = 'draft' | 'active' | 'archived'\nexport type ProductType = 'simple' | 'variable'\nexport type VariantStatus = 'active' | 'inactive'\n\nexport interface ProductMedia {\n url: string\n altText: string\n position: number\n}\n\nexport interface VariantOption {\n name: string\n value: string\n}\n\nexport interface VariantDimensions {\n lengthMm?: number\n widthMm?: number\n heightMm?: number\n}\n\nexport interface Variant {\n id: string\n productId: string\n sku: string\n name: string\n priceMinor: number\n compareAtPriceMinor: number | null\n weightGrams: number | null\n dimensions: VariantDimensions | null\n stock: number\n lowStockThreshold: number\n options: VariantOption[]\n status: VariantStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface Product {\n id: string\n slug: string\n name: string\n description: string\n shortDescription: string\n status: ProductStatus\n type: ProductType\n seoTitle: string\n seoDescription: string\n media: ProductMedia[]\n metadata: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/** The orders a product listing can be requested in; ties break by slug, ascending. */\nexport const PRODUCT_SORT_KEYS = [\n 'newest',\n 'price-asc',\n 'price-desc',\n 'name-asc',\n 'name-desc',\n] as const\nexport type ProductSortKey = (typeof PRODUCT_SORT_KEYS)[number]\nexport const DEFAULT_PRODUCT_SORT: ProductSortKey = 'newest'\n","import { StatusTransitionError } from '../errors.js'\n\nexport type OrderStatus =\n | 'pending'\n | 'confirmed'\n | 'processing'\n | 'shipped'\n | 'delivered'\n | 'cancelled'\n | 'refunded'\n | 'partially_refunded'\n\n// A captured order is refunded from any status between confirmed and delivered; a partial refund leaves room for more.\nexport const ORDER_STATUS_TRANSITIONS: Record<OrderStatus, readonly OrderStatus[]> = {\n pending: ['confirmed', 'cancelled'],\n confirmed: ['processing', 'cancelled', 'refunded', 'partially_refunded'],\n processing: ['shipped', 'cancelled', 'refunded', 'partially_refunded'],\n shipped: ['delivered', 'refunded', 'partially_refunded'],\n delivered: ['refunded', 'partially_refunded'],\n cancelled: [],\n refunded: [],\n partially_refunded: ['refunded'],\n}\n\nexport function canTransitionOrder(from: OrderStatus, to: OrderStatus): boolean {\n return ORDER_STATUS_TRANSITIONS[from].includes(to)\n}\n\nexport function assertOrderTransition(from: OrderStatus, to: OrderStatus): void {\n if (!canTransitionOrder(from, to)) {\n throw new StatusTransitionError('order', from, to)\n }\n}\n\nexport interface Address {\n firstName: string\n lastName: string\n company?: string\n addressLine1: string\n addressLine2?: string\n city: string\n region: string\n postalCode: string\n country: string\n phone?: string\n}\n\nexport interface OrderLineItem {\n variantId: string\n productId: string\n productName: string\n variantName: string\n sku: string\n qty: number\n unitPriceMinor: number\n lineSubtotalMinor: number\n lineTaxMinor: number\n lineTotalMinor: number\n imageUrl: string\n}\n\nexport interface Order {\n id: string\n orderNumber: string\n sequence: number\n customerId: string | null\n status: OrderStatus\n email: string\n billingAddress: Address\n shippingAddress: Address\n lineItems: OrderLineItem[]\n subtotalMinor: number\n taxTotalMinor: number\n shippingTotalMinor: number\n discountTotalMinor: number\n totalMinor: number\n currency: string\n notes: string\n metadata: Record<string, unknown>\n paymentIntentId: string | null\n /** Money taken for the order, in minor units of its currency; 0 while nothing is captured. */\n capturedMinor: number\n capturedAt: string | null\n /** Σ of the order's succeeded refunds (or, for an import or a pre-refund-table order, what was refunded then). */\n refundedMinor: number\n captureMethod: CaptureMethod | null\n createdAt: string\n updatedAt: string\n}\n\n/** Who took the money: the payments provider, the merchant recording an offline payment, or an imported history. */\nexport type CaptureMethod = 'provider' | 'manual' | 'import'\n\nexport type OrderRefundStatus = 'pending' | 'succeeded' | 'failed'\n\n/** One refund of an order. `pending` until the provider's answer settles it, and counted against what is refundable. */\nexport interface OrderRefund {\n id: string\n orderId: string\n amountMinor: number\n currency: string\n providerRefundId: string | null\n status: OrderRefundStatus\n reason: string | null\n actor: string\n createdAt: string\n}\n\nexport type PaymentState = 'unpaid' | 'paid' | 'partially_refunded' | 'refunded'\n\n/** What the admin shows about an order's money: derived from the stored amounts and refunds alone. */\nexport interface OrderPayment {\n state: PaymentState\n capturedMinor: number\n refundedMinor: number\n refundableMinor: number\n currency: string\n refunds: OrderRefund[]\n}\n\nexport function paymentState(order: Pick<Order, 'capturedMinor' | 'refundedMinor'>): PaymentState {\n if (order.capturedMinor <= 0) return 'unpaid'\n if (order.refundedMinor <= 0) return 'paid'\n return order.refundedMinor < order.capturedMinor ? 'partially_refunded' : 'refunded'\n}\n\n/** What can still be refunded: the capture less what was refunded and what is still pending with the provider. */\nexport function refundableMinor(\n order: Pick<Order, 'capturedMinor' | 'refundedMinor'>,\n refunds: ReadonlyArray<Pick<OrderRefund, 'amountMinor' | 'status'>>,\n): number {\n const pending = refunds\n .filter((r) => r.status === 'pending')\n .reduce((sum, r) => sum + r.amountMinor, 0)\n return Math.max(0, order.capturedMinor - order.refundedMinor - pending)\n}\n\nexport interface OrderHistoryEntry {\n id: string\n orderId: string\n fromStatus: OrderStatus | null\n toStatus: OrderStatus\n actor: string\n note: string\n timestamp: string\n}\n","// Product\nexport const EVT_PRODUCT_CREATED = 'product.created.v1'\nexport const EVT_PRODUCT_UPDATED = 'product.updated.v1'\nexport const EVT_PRODUCT_DELETED = 'product.deleted.v1'\nexport const EVT_PRODUCT_PUBLISHED = 'product.published.v1'\nexport const EVT_PRODUCT_UNPUBLISHED = 'product.unpublished.v1'\n\n// Category\nexport const EVT_CATEGORY_CREATED = 'category.created.v1'\nexport const EVT_CATEGORY_UPDATED = 'category.updated.v1'\nexport const EVT_CATEGORY_DELETED = 'category.deleted.v1'\n\n// Order\nexport const EVT_ORDER_PLACED = 'order.placed.v1'\nexport const EVT_ORDER_PAID = 'order.paid.v1'\nexport const EVT_ORDER_FULFILLED = 'order.fulfilled.v1'\nexport const EVT_ORDER_SHIPPED = 'order.shipped.v1'\nexport const EVT_ORDER_DELIVERED = 'order.delivered.v1'\nexport const EVT_ORDER_CANCELLED = 'order.cancelled.v1'\nexport const EVT_ORDER_REFUNDED = 'order.refunded.v1'\nexport const EVT_ORDER_PARTIALLY_REFUNDED = 'order.partially_refunded.v1'\n\n// Inventory\nexport const EVT_INVENTORY_CHANGED = 'inventory.changed.v1'\nexport const EVT_INVENTORY_RESERVED = 'inventory.reserved.v1'\nexport const EVT_INVENTORY_RELEASED = 'inventory.released.v1'\nexport const EVT_INVENTORY_LOW_STOCK = 'inventory.low_stock.v1'\nexport const EVT_INVENTORY_OUT_OF_STOCK = 'inventory.out_of_stock.v1'\n\n// Customer\nexport const EVT_CUSTOMER_CREATED = 'customer.created.v1'\nexport const EVT_CUSTOMER_UPDATED = 'customer.updated.v1'\nexport const EVT_CUSTOMER_DELETED = 'customer.deleted.v1'\n// Legacy alias (kept so Prompt 1 seed code continues to compile).\nexport const EVT_CUSTOMER_REGISTERED = EVT_CUSTOMER_CREATED\n\n// Cart\nexport const EVT_CART_CREATED = 'cart.created.v1'\nexport const EVT_CART_ITEM_ADDED = 'cart.item_added.v1'\nexport const EVT_CART_ITEM_REMOVED = 'cart.item_removed.v1'\nexport const EVT_CART_UPDATED = 'cart.updated.v1'\nexport const EVT_CART_ABANDONED = 'cart.abandoned.v1'\nexport const EVT_CART_CONVERTED = 'cart.converted.v1'\n\n// Payment\nexport const EVT_PAYMENT_CAPTURED = 'payment.captured.v1'\nexport const EVT_PAYMENT_FAILED = 'payment.failed.v1'\nexport const EVT_PAYMENT_REFUNDED = 'payment.refunded.v1'\n\n// System\nexport const EVT_ADAPTER_ERROR = 'adapter.error.v1'\n\nexport const STANDARD_EVENT_TYPES = [\n EVT_PRODUCT_CREATED,\n EVT_PRODUCT_UPDATED,\n EVT_PRODUCT_DELETED,\n EVT_PRODUCT_PUBLISHED,\n EVT_PRODUCT_UNPUBLISHED,\n EVT_CATEGORY_CREATED,\n EVT_CATEGORY_UPDATED,\n EVT_CATEGORY_DELETED,\n EVT_ORDER_PLACED,\n EVT_ORDER_PAID,\n EVT_ORDER_FULFILLED,\n EVT_ORDER_SHIPPED,\n EVT_ORDER_DELIVERED,\n EVT_ORDER_CANCELLED,\n EVT_ORDER_REFUNDED,\n EVT_ORDER_PARTIALLY_REFUNDED,\n EVT_INVENTORY_CHANGED,\n EVT_INVENTORY_RESERVED,\n EVT_INVENTORY_RELEASED,\n EVT_INVENTORY_LOW_STOCK,\n EVT_INVENTORY_OUT_OF_STOCK,\n EVT_CUSTOMER_CREATED,\n EVT_CUSTOMER_UPDATED,\n EVT_CUSTOMER_DELETED,\n EVT_CART_CREATED,\n EVT_CART_ITEM_ADDED,\n EVT_CART_ITEM_REMOVED,\n EVT_CART_UPDATED,\n EVT_CART_ABANDONED,\n EVT_CART_CONVERTED,\n EVT_PAYMENT_CAPTURED,\n EVT_PAYMENT_FAILED,\n EVT_PAYMENT_REFUNDED,\n EVT_ADAPTER_ERROR,\n] as const\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/domain/money.ts","../src/domain/product.ts","../src/domain/order.ts","../src/events/constants.ts","../src/adapters/adapter-base.ts","../src/adapters/document-order.ts","../src/adapters/email-templates.ts","../src/stock-level.ts"],"sourcesContent":["export type ErrorContext = Record<string, unknown>\n\nexport class NymbalError extends Error {\n readonly code: string\n readonly context: ErrorContext\n\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined)\n this.name = this.constructor.name\n this.code = code\n this.context = options.context ?? {}\n }\n\n toJSON(): { name: string; code: string; message: string; context: ErrorContext } {\n return { name: this.name, code: this.code, message: this.message, context: this.context }\n }\n}\n\nexport class ConfigError extends NymbalError {\n constructor(\n message: string,\n options: {\n cause?: unknown\n context?: ErrorContext\n code?: `config.${string}` | `adapter.${string}`\n } = {},\n ) {\n super(options.code ?? 'config.invalid', message, options)\n }\n}\n\nexport class ValidationError extends NymbalError {\n constructor(message: string, options: { cause?: unknown; context?: ErrorContext } = {}) {\n super('validation.failed', message, options)\n }\n}\n\nexport class NotFoundError extends NymbalError {\n constructor(\n resource: string,\n identifier: unknown,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super('not_found', `${resource} not found: ${String(identifier)}`, {\n ...options,\n context: { resource, identifier, ...(options.context ?? {}) },\n })\n }\n}\n\nexport class AdapterError extends NymbalError {\n constructor(\n code: string,\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`adapter.${code}`, message, options)\n }\n}\n\nexport class HttpError extends NymbalError {\n readonly status: number\n\n constructor(\n status: number,\n message: string,\n options: { cause?: unknown; context?: ErrorContext; code?: string } = {},\n ) {\n super(options.code ?? `http.${status}`, message, options)\n this.status = status\n }\n}\n\nexport class AuthError extends HttpError {\n constructor(\n code: 'unauthorized' | 'forbidden' | 'invalid_credentials' | 'token_expired' | 'token_invalid',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n const status = code === 'forbidden' ? 403 : 401\n super(status, message, { ...options, code: `auth.${code}` })\n }\n}\n\nexport class PaymentError extends NymbalError {\n constructor(\n code:\n | 'intent_failed'\n | 'capture_failed'\n // A definite refusal: no money moved.\n | 'refund_rejected'\n // An outcome the shop cannot know (a timeout, a provider fault): money may have moved.\n | 'refund_unknown'\n | 'cancel_refused'\n | 'webhook_invalid'\n | 'not_configured',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`payment.${code}`, message, options)\n }\n}\n\n/** A command the order's money does not allow (confirm unpaid, cancel paid, refund beyond the capture). */\nexport class OrderConflictError extends NymbalError {\n constructor(\n code:\n | 'order.unpaid'\n | 'order.paid'\n | 'order.stock_unavailable'\n | 'order.payment_in_progress'\n | 'refund.not_captured'\n | 'refund.exceeds_captured'\n | 'refund.request_reused',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(code, message, options)\n }\n}\n\nexport class CheckoutError extends NymbalError {\n constructor(\n code:\n | 'cart_empty'\n | 'in_progress'\n | 'payment_in_progress'\n | 'variant_missing'\n | 'out_of_stock'\n | 'reservation_failed'\n | 'finalize_failed',\n message: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(`checkout.${code}`, message, options)\n }\n}\n\nexport class StatusTransitionError extends NymbalError {\n constructor(\n entity: string,\n fromStatus: string,\n toStatus: string,\n options: { cause?: unknown; context?: ErrorContext } = {},\n ) {\n super(\n 'status.invalid_transition',\n `Invalid ${entity} status transition: ${fromStatus} → ${toStatus}`,\n {\n ...options,\n context: { entity, fromStatus, toStatus, ...(options.context ?? {}) },\n },\n )\n }\n}\n","import { ValidationError } from '../errors.js'\n\nexport interface Money {\n amount: number\n currency: string\n}\n\nexport function money(amount: number, currency: string): Money {\n assertMinorUnits(amount)\n return { amount, currency }\n}\n\nexport function assertMinorUnits(amount: number): void {\n if (!Number.isInteger(amount)) {\n throw new ValidationError(`Monetary amount must be an integer (minor units), got ${amount}`)\n }\n}\n\nfunction assertSameCurrency(a: Money, b: Money): void {\n if (a.currency !== b.currency) {\n throw new ValidationError(\n `Currency mismatch: ${a.currency} vs ${b.currency}`,\n )\n }\n}\n\nexport function add(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount + b.amount, currency: a.currency }\n}\n\nexport function sub(a: Money, b: Money): Money {\n assertSameCurrency(a, b)\n return { amount: a.amount - b.amount, currency: a.currency }\n}\n\nexport function mulBy(m: Money, scalar: number): Money {\n if (!Number.isFinite(scalar)) {\n throw new ValidationError(`Scalar must be finite, got ${scalar}`)\n }\n return { amount: Math.round(m.amount * scalar), currency: m.currency }\n}\n\nexport function sumMoney(values: Money[], currency: string): Money {\n const total = values.reduce((acc, m) => {\n assertSameCurrency(m, { amount: 0, currency })\n return acc + m.amount\n }, 0)\n return { amount: total, currency }\n}\n\nexport function allocate(m: Money, weights: number[]): Money[] {\n if (weights.length === 0) return []\n const totalWeight = weights.reduce((s, w) => s + w, 0)\n if (totalWeight <= 0) {\n throw new ValidationError('allocate: sum of weights must be > 0')\n }\n const portions = weights.map((w) => Math.floor((m.amount * w) / totalWeight))\n let remainder = m.amount - portions.reduce((s, p) => s + p, 0)\n const result = portions.slice()\n let idx = 0\n while (remainder !== 0 && idx < result.length) {\n const delta = remainder > 0 ? 1 : -1\n result[idx] = (result[idx] ?? 0) + delta\n remainder -= delta\n idx += 1\n }\n return result.map((amount) => ({ amount, currency: m.currency }))\n}\n","export type ProductStatus = 'draft' | 'active' | 'archived'\nexport type ProductType = 'simple' | 'variable'\nexport type VariantStatus = 'active' | 'inactive'\n\nexport interface ProductMedia {\n url: string\n altText: string\n position: number\n}\n\nexport interface VariantOption {\n name: string\n value: string\n}\n\nexport interface VariantDimensions {\n lengthMm?: number\n widthMm?: number\n heightMm?: number\n}\n\nexport interface Variant {\n id: string\n productId: string\n sku: string\n name: string\n priceMinor: number\n compareAtPriceMinor: number | null\n weightGrams: number | null\n dimensions: VariantDimensions | null\n stock: number\n lowStockThreshold: number\n options: VariantOption[]\n status: VariantStatus\n createdAt: string\n updatedAt: string\n}\n\nexport interface Product {\n id: string\n slug: string\n name: string\n description: string\n shortDescription: string\n status: ProductStatus\n type: ProductType\n seoTitle: string\n seoDescription: string\n media: ProductMedia[]\n metadata: Record<string, unknown>\n createdAt: string\n updatedAt: string\n}\n\n/** The orders a product listing can be requested in; ties break by slug, ascending. */\nexport const PRODUCT_SORT_KEYS = [\n 'newest',\n 'price-asc',\n 'price-desc',\n 'name-asc',\n 'name-desc',\n] as const\nexport type ProductSortKey = (typeof PRODUCT_SORT_KEYS)[number]\nexport const DEFAULT_PRODUCT_SORT: ProductSortKey = 'newest'\n/** What a storefront shows for each sort. */\nexport const PRODUCT_SORT_LABELS: Readonly<Record<ProductSortKey, string>> = {\n newest: 'Newest',\n 'price-asc': 'Price: Low to High',\n 'price-desc': 'Price: High to Low',\n 'name-asc': 'Name: A to Z',\n 'name-desc': 'Name: Z to A',\n}\n","import { StatusTransitionError } from '../errors.js'\n\nexport type OrderStatus =\n | 'pending'\n | 'confirmed'\n | 'processing'\n | 'shipped'\n | 'delivered'\n | 'cancelled'\n | 'refunded'\n | 'partially_refunded'\n\n// A captured order is refunded from any status between confirmed and delivered; a partial refund leaves room for more.\nexport const ORDER_STATUS_TRANSITIONS: Record<OrderStatus, readonly OrderStatus[]> = {\n pending: ['confirmed', 'cancelled'],\n confirmed: ['processing', 'cancelled', 'refunded', 'partially_refunded'],\n processing: ['shipped', 'cancelled', 'refunded', 'partially_refunded'],\n shipped: ['delivered', 'refunded', 'partially_refunded'],\n delivered: ['refunded', 'partially_refunded'],\n cancelled: [],\n refunded: [],\n partially_refunded: ['refunded'],\n}\n\nexport function canTransitionOrder(from: OrderStatus, to: OrderStatus): boolean {\n return ORDER_STATUS_TRANSITIONS[from].includes(to)\n}\n\nexport function assertOrderTransition(from: OrderStatus, to: OrderStatus): void {\n if (!canTransitionOrder(from, to)) {\n throw new StatusTransitionError('order', from, to)\n }\n}\n\nexport interface Address {\n firstName: string\n lastName: string\n company?: string\n addressLine1: string\n addressLine2?: string\n city: string\n region: string\n postalCode: string\n country: string\n phone?: string\n}\n\nexport interface OrderLineItem {\n variantId: string\n productId: string\n productName: string\n variantName: string\n sku: string\n qty: number\n unitPriceMinor: number\n lineSubtotalMinor: number\n lineTaxMinor: number\n lineTotalMinor: number\n imageUrl: string\n}\n\nexport interface Order {\n id: string\n orderNumber: string\n sequence: number\n customerId: string | null\n status: OrderStatus\n email: string\n billingAddress: Address\n shippingAddress: Address\n lineItems: OrderLineItem[]\n subtotalMinor: number\n taxTotalMinor: number\n shippingTotalMinor: number\n discountTotalMinor: number\n totalMinor: number\n currency: string\n notes: string\n metadata: Record<string, unknown>\n paymentIntentId: string | null\n /** Money taken for the order, in minor units of its currency; 0 while nothing is captured. */\n capturedMinor: number\n capturedAt: string | null\n /** Σ of the order's succeeded refunds (or, for an import or a pre-refund-table order, what was refunded then). */\n refundedMinor: number\n captureMethod: CaptureMethod | null\n createdAt: string\n updatedAt: string\n}\n\n/** Who took the money: the payments provider, the merchant recording an offline payment, or an imported history. */\nexport type CaptureMethod = 'provider' | 'manual' | 'import'\n\nexport type OrderRefundStatus = 'pending' | 'succeeded' | 'failed'\n\n/** One refund of an order. `pending` until the provider's answer settles it, and counted against what is refundable. */\nexport interface OrderRefund {\n id: string\n orderId: string\n amountMinor: number\n currency: string\n providerRefundId: string | null\n status: OrderRefundStatus\n reason: string | null\n actor: string\n createdAt: string\n}\n\nexport type PaymentState = 'unpaid' | 'paid' | 'partially_refunded' | 'refunded'\n\n/** What the admin shows about an order's money: derived from the stored amounts and refunds alone. */\nexport interface OrderPayment {\n state: PaymentState\n capturedMinor: number\n refundedMinor: number\n refundableMinor: number\n currency: string\n refunds: OrderRefund[]\n}\n\nexport function paymentState(order: Pick<Order, 'capturedMinor' | 'refundedMinor'>): PaymentState {\n if (order.capturedMinor <= 0) return 'unpaid'\n if (order.refundedMinor <= 0) return 'paid'\n return order.refundedMinor < order.capturedMinor ? 'partially_refunded' : 'refunded'\n}\n\n/** What can still be refunded: the capture less what was refunded and what is still pending with the provider. */\nexport function refundableMinor(\n order: Pick<Order, 'capturedMinor' | 'refundedMinor'>,\n refunds: ReadonlyArray<Pick<OrderRefund, 'amountMinor' | 'status'>>,\n): number {\n const pending = refunds\n .filter((r) => r.status === 'pending')\n .reduce((sum, r) => sum + r.amountMinor, 0)\n return Math.max(0, order.capturedMinor - order.refundedMinor - pending)\n}\n\nexport interface OrderHistoryEntry {\n id: string\n orderId: string\n fromStatus: OrderStatus | null\n toStatus: OrderStatus\n actor: string\n note: string\n timestamp: string\n}\n","// Product\nexport const EVT_PRODUCT_CREATED = 'product.created.v1'\nexport const EVT_PRODUCT_UPDATED = 'product.updated.v1'\nexport const EVT_PRODUCT_DELETED = 'product.deleted.v1'\nexport const EVT_PRODUCT_PUBLISHED = 'product.published.v1'\nexport const EVT_PRODUCT_UNPUBLISHED = 'product.unpublished.v1'\n\n// Category\nexport const EVT_CATEGORY_CREATED = 'category.created.v1'\nexport const EVT_CATEGORY_UPDATED = 'category.updated.v1'\nexport const EVT_CATEGORY_DELETED = 'category.deleted.v1'\n\n// Order\nexport const EVT_ORDER_PLACED = 'order.placed.v1'\nexport const EVT_ORDER_PAID = 'order.paid.v1'\nexport const EVT_ORDER_FULFILLED = 'order.fulfilled.v1'\nexport const EVT_ORDER_SHIPPED = 'order.shipped.v1'\nexport const EVT_ORDER_DELIVERED = 'order.delivered.v1'\nexport const EVT_ORDER_CANCELLED = 'order.cancelled.v1'\nexport const EVT_ORDER_REFUNDED = 'order.refunded.v1'\nexport const EVT_ORDER_PARTIALLY_REFUNDED = 'order.partially_refunded.v1'\n\n// Inventory\nexport const EVT_INVENTORY_CHANGED = 'inventory.changed.v1'\nexport const EVT_INVENTORY_RESERVED = 'inventory.reserved.v1'\nexport const EVT_INVENTORY_RELEASED = 'inventory.released.v1'\nexport const EVT_INVENTORY_LOW_STOCK = 'inventory.low_stock.v1'\nexport const EVT_INVENTORY_OUT_OF_STOCK = 'inventory.out_of_stock.v1'\n\n// Customer\nexport const EVT_CUSTOMER_CREATED = 'customer.created.v1'\nexport const EVT_CUSTOMER_UPDATED = 'customer.updated.v1'\nexport const EVT_CUSTOMER_DELETED = 'customer.deleted.v1'\n// Legacy alias (kept so Prompt 1 seed code continues to compile).\nexport const EVT_CUSTOMER_REGISTERED = EVT_CUSTOMER_CREATED\n\n// Cart\nexport const EVT_CART_CREATED = 'cart.created.v1'\nexport const EVT_CART_ITEM_ADDED = 'cart.item_added.v1'\nexport const EVT_CART_ITEM_REMOVED = 'cart.item_removed.v1'\nexport const EVT_CART_UPDATED = 'cart.updated.v1'\nexport const EVT_CART_ABANDONED = 'cart.abandoned.v1'\nexport const EVT_CART_CONVERTED = 'cart.converted.v1'\n\n// Payment\nexport const EVT_PAYMENT_CAPTURED = 'payment.captured.v1'\nexport const EVT_PAYMENT_FAILED = 'payment.failed.v1'\nexport const EVT_PAYMENT_REFUNDED = 'payment.refunded.v1'\n\n// System\nexport const EVT_ADAPTER_ERROR = 'adapter.error.v1'\n\nexport const STANDARD_EVENT_TYPES = [\n EVT_PRODUCT_CREATED,\n EVT_PRODUCT_UPDATED,\n EVT_PRODUCT_DELETED,\n EVT_PRODUCT_PUBLISHED,\n EVT_PRODUCT_UNPUBLISHED,\n EVT_CATEGORY_CREATED,\n EVT_CATEGORY_UPDATED,\n EVT_CATEGORY_DELETED,\n EVT_ORDER_PLACED,\n EVT_ORDER_PAID,\n EVT_ORDER_FULFILLED,\n EVT_ORDER_SHIPPED,\n EVT_ORDER_DELIVERED,\n EVT_ORDER_CANCELLED,\n EVT_ORDER_REFUNDED,\n EVT_ORDER_PARTIALLY_REFUNDED,\n EVT_INVENTORY_CHANGED,\n EVT_INVENTORY_RESERVED,\n EVT_INVENTORY_RELEASED,\n EVT_INVENTORY_LOW_STOCK,\n EVT_INVENTORY_OUT_OF_STOCK,\n EVT_CUSTOMER_CREATED,\n EVT_CUSTOMER_UPDATED,\n EVT_CUSTOMER_DELETED,\n EVT_CART_CREATED,\n EVT_CART_ITEM_ADDED,\n EVT_CART_ITEM_REMOVED,\n EVT_CART_UPDATED,\n EVT_CART_ABANDONED,\n EVT_CART_CONVERTED,\n EVT_PAYMENT_CAPTURED,\n EVT_PAYMENT_FAILED,\n EVT_PAYMENT_REFUNDED,\n EVT_ADAPTER_ERROR,\n] as const\n","import type { Logger } from './http.js'\n\nexport type HealthStatus = 'healthy' | 'degraded' | 'down'\n\nexport interface HealthReport {\n status: HealthStatus\n detail?: string\n}\n\n/** How long a health check may run before it reports `down`. */\nexport const HEALTH_CHECK_TIMEOUT_MS = 5000\n\nexport interface BoundedHealthCheckOptions {\n /** `HEALTH_CHECK_TIMEOUT_MS` when not given. */\n timeoutMs?: number\n /** Values an error's message must never show (a URL's user, password or query values); each becomes `***`. */\n redact?: readonly string[]\n}\n\n/** An error's message for a report; an AggregateError (a refused connection to every address of a name) lists its own. */\nexport function errorText(error: unknown): string {\n if (!(error instanceof Error)) return String(error)\n if (error.message !== '') return error.message\n const inner = (error as { errors?: unknown }).errors\n if (Array.isArray(inner) && inner.length > 0) return inner.map(errorText).join('; ')\n const code = (error as { code?: unknown }).code\n return typeof code === 'string' ? code : error.name\n}\n\n/**\n * The shortest value worth removing from text as a secret. A shorter one is too likely to be an ordinary word (a local\n * password of `nymbal`), whose removal garbles the text; user info and query strings go with the URL rewrite anyway.\n */\nexport const SECRET_MIN_LENGTH = 8\n\n/**\n * The values in a connection URL that are credentials or may carry one: its password and its query values, and its\n * user unless `user` is false, each as written and decoded, if at least `SECRET_MIN_LENGTH` long. A user is worth\n * hiding in a driver's error, not in our own text.\n */\nexport function connectionSecrets(url: string, options: { user?: boolean } = {}): string[] {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n return []\n }\n const raw = [\n ...(options.user === false ? [] : [parsed.username]),\n parsed.password,\n ...parsed.searchParams.values(),\n ]\n const decoded = raw.map((value) => {\n try {\n return decodeURIComponent(value)\n } catch {\n return value\n }\n })\n return [...new Set([...raw, ...decoded])].filter((value) => value.length >= SECRET_MIN_LENGTH)\n}\n\n/**\n * A connection URL as it may be printed: `scheme://host:port/path`, with no user, password, query or fragment. Text\n * that is not a URL comes back as `<unparsable url>`, since it may be a URL with a secret the parser could not read.\n */\nexport function displayUrl(url: string): string {\n try {\n const parsed = new URL(url)\n return `${parsed.protocol}//${parsed.host}${parsed.pathname === '/' ? '' : parsed.pathname}`\n } catch {\n return '<unparsable url>'\n }\n}\n\n/**\n * Runs `check` within the bound. A check that throws is `down` with the error's message, every `redact` value in it\n * replaced; one still running when the bound passes is `down` with `timed out after <n> ms`, and its signal aborted.\n */\nexport async function boundedHealthCheck(\n check: (signal: AbortSignal) => Promise<HealthReport> | HealthReport,\n options: BoundedHealthCheckOptions = {},\n): Promise<HealthReport> {\n const timeoutMs = options.timeoutMs ?? HEALTH_CHECK_TIMEOUT_MS\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const timedOut = new Promise<HealthReport>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve({ status: 'down', detail: `timed out after ${timeoutMs} ms` })\n }, timeoutMs)\n })\n const failed = (error: unknown): HealthReport => {\n let detail = errorText(error)\n for (const secret of options.redact ?? []) {\n if (secret !== '') detail = detail.split(secret).join('***')\n }\n return { status: 'down', detail }\n }\n try {\n // Started inside a promise so a check that throws before returning one, or returns a plain report, is still\n // mapped rather than escaping the caller.\n const checked = Promise.resolve()\n .then(() => check(controller.signal))\n .catch(failed)\n return await Promise.race([checked, timedOut])\n } finally {\n clearTimeout(timer)\n }\n}\n\nexport interface AdapterBase {\n /** What this adapter offers, for the app's per-slot report; each adapter answers for itself. */\n capabilities(): readonly string[]\n readonly producesEvents: readonly string[]\n readonly consumesEvents: readonly string[]\n initialize(config: unknown): Promise<void>\n healthCheck(): Promise<HealthReport>\n}\n\n/** The commerce slots a store's config names a provider for. */\nexport type CommerceSlot =\n | 'payments'\n | 'email'\n | 'reviews'\n | 'search'\n | 'analytics'\n | 'shipping'\n | 'tax'\n | 'ai'\n\n/** Every slot an adapter package can fill: the commerce slots and the infrastructure primitives. */\nexport type Slot = CommerceSlot | 'documentStore' | 'eventBus' | 'media'\n\n/**\n * What the kernel hands an adapter package's `createAdapter`. A package that serves more than one slot switches on\n * `slot`; `config` is a commerce slot's `config` block from nymbal.config.ts, or a primitive's connection block.\n */\nexport interface AdapterContext {\n slot: Slot\n config: Record<string, unknown>\n logger: Logger\n env: NodeJS.ProcessEnv\n}\n","import { ValidationError } from '../errors.js'\nimport type { QueryParams, QueryResult } from './document-store.js'\n\n// The one order and filter every document store is held to: the in-memory store runs it, and stores that cannot\n// order or filter the same way on their server fetch candidates and run it in the process.\n\nconst collator = new Intl.Collator(undefined, { sensitivity: 'base' })\n\n/** Number, then string, then any other type, then null and missing: so values of different types never tie. */\nfunction rankOf(value: unknown): number {\n if (typeof value === 'number') return 0\n if (typeof value === 'string') return 1\n if (value === null || value === undefined) return 3\n return 2\n}\n\nfunction sign(n: number): number {\n return n > 0 ? 1 : n < 0 ? -1 : 0\n}\n\nfunction compareOther(a: unknown, b: unknown): number {\n const ta = typeof a\n const tb = typeof b\n if (ta !== tb) return ta < tb ? -1 : 1\n if (ta === 'boolean') return Number(a) - Number(b)\n const ja = JSON.stringify(a) ?? ''\n const jb = JSON.stringify(b) ?? ''\n return ja < jb ? -1 : ja > jb ? 1 : 0\n}\n\n/**\n * A total order over document values: numbers numerically, strings by `Intl.Collator` at base sensitivity (case and\n * accents tie), other types after strings, and null or missing last. Values of different types rank by type first.\n */\nexport function compareDocumentValues(a: unknown, b: unknown): number {\n const ra = rankOf(a)\n const rb = rankOf(b)\n if (ra !== rb) return ra - rb\n switch (ra) {\n case 0:\n return sign((a as number) - (b as number))\n case 1:\n return sign(collator.compare(a as string, b as string))\n case 2:\n return sign(compareOther(a, b))\n default:\n return 0\n }\n}\n\nexport type SortKeyCondition = Pick<NonNullable<QueryParams['sortKey']>, 'operator' | 'value'>\n\n/** `eq` is `===`; `gt`, `lt` and `between` (inclusive, `[low, high]`) are `compareDocumentValues`. */\nexport function matchesSortKey(value: unknown, condition: SortKeyCondition): boolean {\n const target = condition.value\n switch (condition.operator) {\n case 'eq':\n return value === target\n case 'gt':\n return compareDocumentValues(value, target) > 0\n case 'lt':\n return compareDocumentValues(value, target) < 0\n case 'between': {\n if (!Array.isArray(target) || target.length !== 2) return false\n const [low, high] = target as [unknown, unknown]\n return compareDocumentValues(value, low) >= 0 && compareDocumentValues(value, high) <= 0\n }\n default:\n return false\n }\n}\n\n/** The value at a dotted path (`priceRange.minMinor`); missing anywhere along it is `undefined`. */\nexport function documentValueAt(doc: unknown, path: string): unknown {\n let value: unknown = doc\n for (const part of path.split('.')) {\n if (value === null || typeof value !== 'object') return undefined\n value = (value as Record<string, unknown>)[part]\n }\n return value\n}\n\nexport const DEFAULT_QUERY_LIMIT = 50\n\ninterface Order {\n /** The field ordered by, or null for the document key alone. */\n field: string | null\n direction: 'asc' | 'desc'\n}\n\ninterface Position {\n key: string\n value: unknown\n}\n\nfunction orderOf(params: QueryParams): Order {\n if (params.orderBy !== undefined) {\n return { field: params.orderBy.field, direction: params.orderBy.direction }\n }\n const direction = (params.ascending ?? true) ? 'asc' : 'desc'\n return { field: params.sortKey?.field ?? null, direction }\n}\n\nconst isNullish = (value: unknown): boolean => value === null || value === undefined\n\n/** Nulls and missing values last in either direction; ties by key ascending, so no two documents tie. */\nfunction comparePositions(a: Position, b: Position, order: Order): number {\n if (order.field !== null) {\n const an = isNullish(a.value)\n const bn = isNullish(b.value)\n if (an !== bn) return an ? 1 : -1\n let c = an ? 0 : compareDocumentValues(a.value, b.value)\n if (order.direction === 'desc') c = -c\n if (c !== 0) return c\n return a.key < b.key ? -1 : a.key > b.key ? 1 : 0\n }\n const c = a.key < b.key ? -1 : a.key > b.key ? 1 : 0\n return order.direction === 'desc' ? -c : c\n}\n\n/** What a cursor is bound to: the order and the partition, so a cursor reused under another is refused. */\nfunction cursorScope(order: Order, params: QueryParams): string {\n return JSON.stringify([order.field, order.direction, params.partitionKey])\n}\n\nfunction decodeCursor(cursor: string, scope: string): Position {\n let decoded: unknown\n try {\n decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'))\n } catch {\n decoded = null\n }\n const c = decoded as { o?: unknown; k?: unknown; v?: unknown } | null\n if (c === null || typeof c !== 'object' || typeof c.o !== 'string' || typeof c.k !== 'string') {\n throw new ValidationError('The cursor is not valid', { context: { cursor } })\n }\n if (c.o !== scope) {\n throw new ValidationError('The cursor was issued under another order', { context: { cursor } })\n }\n return { key: c.k, value: c.v ?? null }\n}\n\n/**\n * One page of `params` over the documents of a collection: filter by partition (`'*'` for all) and sort key, order,\n * then seek past the cursor by value, so a document deleted between pages neither restarts nor repeats the walk.\n */\nexport function queryDocuments<T>(\n documents: Iterable<readonly [string, unknown]>,\n params: QueryParams,\n): QueryResult<T> {\n const limit = params.limit ?? DEFAULT_QUERY_LIMIT\n const order = orderOf(params)\n const scope = cursorScope(order, params)\n const after = params.cursor ? decodeCursor(params.cursor, scope) : null\n const { partitionKey, sortKey } = params\n\n const matches: Array<{ position: Position; doc: unknown }> = []\n for (const [key, doc] of documents) {\n const record = (doc ?? {}) as Record<string, unknown>\n if (partitionKey.value !== '*' && record[partitionKey.field] !== partitionKey.value) continue\n if (sortKey !== undefined && !matchesSortKey(record[sortKey.field], sortKey)) continue\n const value = order.field === null ? null : (documentValueAt(record, order.field) ?? null)\n const position = { key, value }\n if (after !== null && comparePositions(position, after, order) <= 0) continue\n matches.push({ position, doc })\n }\n matches.sort((a, b) => comparePositions(a.position, b.position, order))\n\n const page = matches.slice(0, limit)\n const last = page[page.length - 1]\n const nextCursor =\n matches.length > limit && last !== undefined\n ? Buffer.from(\n JSON.stringify({ o: scope, k: last.position.key, v: last.position.value }),\n 'utf8',\n ).toString('base64url')\n : null\n return { items: page.map((m) => m.doc as T), nextCursor }\n}\n","// The kernel's transactional email templates, shared by every email adapter (the console one and the SMTP package).\nexport interface RenderedEmail {\n subject: string\n text: string\n html: string\n}\n\nfunction render(template: string, vars: Record<string, unknown>): string {\n return template.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (_, path: string) => {\n const parts = path.split('.')\n let cur: unknown = vars\n for (const p of parts) {\n if (cur && typeof cur === 'object' && p in (cur as Record<string, unknown>)) {\n cur = (cur as Record<string, unknown>)[p]\n } else {\n return ''\n }\n }\n return cur == null ? '' : String(cur)\n })\n}\n\nconst TEMPLATES: Record<string, { subject: string; body: string }> = {\n 'order-placed': {\n subject: 'Order {{orderNumber}} confirmed',\n body: `Hi {{customerName}},\\n\\nThank you for your order {{orderNumber}}. Total: {{totalFormatted}}.\\n\\nWe'll let you know when it ships.`,\n },\n 'order-shipped': {\n subject: 'Order {{orderNumber}} is on its way',\n body: `Hi {{customerName}},\\n\\nYour order {{orderNumber}} has shipped via {{carrier}}.\\nTracking: {{trackingNumber}}`,\n },\n welcome: {\n subject: 'Welcome to {{storeName}}',\n body: `Welcome {{customerName}}! Your account is ready.`,\n },\n 'cart-abandoned': {\n subject: \"Don't forget your cart at {{storeName}}\",\n body: `Hi,\\n\\nYou left items in your cart. Pick up where you left off: {{resumeUrl}}`,\n },\n}\n\nexport function renderEmailTemplate(\n template: string,\n vars: Record<string, unknown>,\n): RenderedEmail {\n const t = TEMPLATES[template]\n if (!t) {\n return {\n subject: `[nymbal] ${template}`,\n text: JSON.stringify(vars),\n html: `<pre>${JSON.stringify(vars, null, 2)}</pre>`,\n }\n }\n const subject = render(t.subject, vars)\n const text = render(t.body, vars)\n const html = `<pre>${text.replace(/</g, '<')}</pre>`\n return { subject, text, html }\n}\n","export type StockLevel = 'out' | 'low' | 'in'\n\n/** Below this many units a variant is low on stock. */\nconst LOW_STOCK_BELOW = 5\n\n/** The one stock rule every storefront renders from. */\nexport function stockLevel(stock: number): StockLevel {\n if (stock <= 0) return 'out'\n return stock < LOW_STOCK_BELOW ? 'low' : 'in'\n}\n\nexport const STOCK_LABELS: Record<StockLevel, string> = {\n out: 'Out of Stock',\n low: 'Low Stock',\n in: 'In Stock',\n}\n"],"mappings":"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","names":["c"]}
|