@webstir-io/module-contract 0.8.1 → 0.9.1

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.
@@ -15,5 +15,19 @@ export declare const CLIENT_NAV_HEADERS: {
15
15
  /** Identifies one form submission, so the same post sent again gets the first answer. */
16
16
  readonly submission: 'x-webstir-submission';
17
17
  };
18
- /** The form field carrying the submission id when client-nav falls back to a normal post. */
18
+ /**
19
+ * The form field carrying a submission id: rendered into every POST form a view renders, and set by
20
+ * client-nav when it falls back to a normal post.
21
+ */
19
22
  export declare const CLIENT_NAV_SUBMISSION_FIELD = "_webstir_submission";
23
+ /**
24
+ * The form field carrying the address of the page a form was rendered on, so a failed submission
25
+ * goes back to that page even when the browser sends no Referer, or one that names the address of
26
+ * an earlier failed post.
27
+ */
28
+ export declare const FORM_PAGE_FIELD = "_webstir_page";
29
+ /**
30
+ * The form field an app writes into a form that is one of many on a page (one per item), naming
31
+ * the state its failures go to: `<input type="hidden" name="_webstir_form" value="reply:42">`.
32
+ */
33
+ export declare const FORM_ID_FIELD = "_webstir_form";
@@ -15,5 +15,19 @@ export const CLIENT_NAV_HEADERS = {
15
15
  /** Identifies one form submission, so the same post sent again gets the first answer. */
16
16
  submission: 'x-webstir-submission',
17
17
  };
18
- /** The form field carrying the submission id when client-nav falls back to a normal post. */
18
+ /**
19
+ * The form field carrying a submission id: rendered into every POST form a view renders, and set by
20
+ * client-nav when it falls back to a normal post.
21
+ */
19
22
  export const CLIENT_NAV_SUBMISSION_FIELD = '_webstir_submission';
23
+ /**
24
+ * The form field carrying the address of the page a form was rendered on, so a failed submission
25
+ * goes back to that page even when the browser sends no Referer, or one that names the address of
26
+ * an earlier failed post.
27
+ */
28
+ export const FORM_PAGE_FIELD = '_webstir_page';
29
+ /**
30
+ * The form field an app writes into a form that is one of many on a page (one per item), naming
31
+ * the state its failures go to: `<input type="hidden" name="_webstir_form" value="reply:42">`.
32
+ */
33
+ export const FORM_ID_FIELD = '_webstir_form';
package/dist/index.d.ts CHANGED
@@ -98,6 +98,29 @@ export interface FormState {
98
98
  export interface FormStateReader {
99
99
  read(formId: string): FormState;
100
100
  }
101
+ /**
102
+ * What a page binds for one form: whether it came back from a failed submission, what was typed in
103
+ * each field, and the first error per field (and `form`, for errors about the whole form).
104
+ */
105
+ export declare function formStateSchema<const TFields extends readonly string[]>(fields: TFields): z.ZodObject<{
106
+ submitted: z.ZodBoolean;
107
+ values: z.ZodObject<Record<TFields[number], z.ZodOptional<z.ZodString>>, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<Record<TFields[number], z.ZodOptional<z.ZodString>>>, any> extends infer T ? { [k in keyof T]: T[k]; } : never, z.baseObjectInputType<Record<TFields[number], z.ZodOptional<z.ZodString>>> extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>;
108
+ errors: z.ZodObject<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>>, any> extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never, z.baseObjectInputType<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>> extends infer T_3 ? { [k in keyof T_3]: T_3[k]; } : never>;
109
+ }, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<{
110
+ submitted: z.ZodBoolean;
111
+ values: z.ZodObject<Record<TFields[number], z.ZodOptional<z.ZodString>>, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<Record<TFields[number], z.ZodOptional<z.ZodString>>>, any> extends infer T ? { [k in keyof T]: T[k]; } : never, z.baseObjectInputType<Record<TFields[number], z.ZodOptional<z.ZodString>>> extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>;
112
+ errors: z.ZodObject<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>>, any> extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never, z.baseObjectInputType<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>> extends infer T_3 ? { [k in keyof T_3]: T_3[k]; } : never>;
113
+ }>, any> extends infer T_4 ? { [k in keyof T_4]: T_4[k]; } : never, z.baseObjectInputType<{
114
+ submitted: z.ZodBoolean;
115
+ values: z.ZodObject<Record<TFields[number], z.ZodOptional<z.ZodString>>, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<Record<TFields[number], z.ZodOptional<z.ZodString>>>, any> extends infer T ? { [k in keyof T]: T[k]; } : never, z.baseObjectInputType<Record<TFields[number], z.ZodOptional<z.ZodString>>> extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>;
116
+ errors: z.ZodObject<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>>, any> extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never, z.baseObjectInputType<Record<(readonly [...TFields, "form"])[number], z.ZodOptional<z.ZodString>>> extends infer T_3 ? { [k in keyof T_3]: T_3[k]; } : never>;
117
+ }> extends infer T_5 ? { [k in keyof T_5]: T_5[k]; } : never>;
118
+ /** A form's state as `formStateSchema(fields)` binds it, with `defaults` for a fresh form. */
119
+ export declare function formView<const TFields extends readonly string[]>(state: FormState, fields: TFields, defaults?: Partial<Record<TFields[number], string>>): {
120
+ submitted: boolean;
121
+ values: Partial<Record<TFields[number], string>>;
122
+ errors: Partial<Record<TFields[number] | 'form', string>>;
123
+ };
101
124
  export interface SSRContext<TParams = Record<string, string>, TAuth = unknown, TSession = unknown, TEnv extends EnvAccessor = EnvAccessor, TLogger extends Logger = Logger> {
102
125
  readonly url: URL;
103
126
  readonly params: TParams;
@@ -196,6 +219,18 @@ export declare const moduleErrorSchema: z.ZodObject<{
196
219
  correlationId?: string | undefined;
197
220
  }>;
198
221
  export type ModuleError = z.infer<typeof moduleErrorSchema>;
222
+ /**
223
+ * Who reaches a route or view: `'required'` is anyone signed in; `{ role }` is someone signed in
224
+ * whose user has that role. Others are sent to sign in; signed in without the role is a 404.
225
+ */
226
+ export declare const accessRequirementSchema: z.ZodUnion<[z.ZodLiteral<"required">, z.ZodObject<{
227
+ role: z.ZodString;
228
+ }, "strict", z.ZodTypeAny, {
229
+ role: string;
230
+ }, {
231
+ role: string;
232
+ }>]>;
233
+ export type AccessRequirement = z.infer<typeof accessRequirementSchema>;
199
234
  export declare const httpMethodSchema: z.ZodEnum<["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]>;
200
235
  export type HttpMethod = z.infer<typeof httpMethodSchema>;
201
236
  export declare const routeInteractionKindSchema: z.ZodEnum<["navigation", "mutation"]>;
@@ -362,6 +397,17 @@ export declare const routeSessionSchema: z.ZodObject<{
362
397
  export type RouteSessionDefinition = z.infer<typeof routeSessionSchema>;
363
398
  export declare const flashLevelSchema: z.ZodEnum<["info", "success", "warning", "error"]>;
364
399
  export type FlashLevel = z.infer<typeof flashLevelSchema>;
400
+ /** What a page binds as `flash`: the messages an action left for it. The framework supplies it. */
401
+ export declare const flashSchema: z.ZodArray<z.ZodObject<{
402
+ level: z.ZodEnum<["info", "success", "warning", "error"]>;
403
+ message: z.ZodString;
404
+ }, "strip", z.ZodTypeAny, {
405
+ level: "error" | "info" | "success" | "warning";
406
+ message: string;
407
+ }, {
408
+ level: "error" | "info" | "success" | "warning";
409
+ message: string;
410
+ }>, "many">;
365
411
  export declare const flashPublishConditionSchema: z.ZodEnum<["always", "success", "error"]>;
366
412
  export type FlashPublishCondition = z.infer<typeof flashPublishConditionSchema>;
367
413
  export declare const routeFlashMessageSchema: z.ZodObject<{
@@ -418,6 +464,8 @@ export declare const routeFlashSchema: z.ZodObject<{
418
464
  }>;
419
465
  export type RouteFlashDefinition = z.infer<typeof routeFlashSchema>;
420
466
  export declare const routeFormSchema: z.ZodObject<{
467
+ /** The form's id, which the page's loader reads its state by; the route's name by default. */
468
+ id: z.ZodOptional<z.ZodString>;
421
469
  contentType: z.ZodOptional<z.ZodEnum<["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]>>;
422
470
  csrf: z.ZodOptional<z.ZodBoolean>;
423
471
  session: z.ZodOptional<z.ZodObject<{
@@ -466,6 +514,7 @@ export declare const routeFormSchema: z.ZodObject<{
466
514
  }[] | undefined;
467
515
  }>>;
468
516
  }, "strict", z.ZodTypeAny, {
517
+ id?: string | undefined;
469
518
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
470
519
  csrf?: boolean | undefined;
471
520
  session?: {
@@ -482,6 +531,7 @@ export declare const routeFormSchema: z.ZodObject<{
482
531
  }[] | undefined;
483
532
  } | undefined;
484
533
  }, {
534
+ id?: string | undefined;
485
535
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
486
536
  csrf?: boolean | undefined;
487
537
  session?: {
@@ -1603,8 +1653,14 @@ export declare const routeDefinitionSchema: z.ZodObject<{
1603
1653
  mode?: "optional" | "required" | undefined;
1604
1654
  write?: boolean | undefined;
1605
1655
  }>>;
1606
- /** `required`: only a signed-in user reaches the route; others are sent to sign in. */
1607
- auth: z.ZodOptional<z.ZodEnum<["required"]>>;
1656
+ /** Who reaches the route: see `accessRequirementSchema`. */
1657
+ auth: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"required">, z.ZodObject<{
1658
+ role: z.ZodString;
1659
+ }, "strict", z.ZodTypeAny, {
1660
+ role: string;
1661
+ }, {
1662
+ role: string;
1663
+ }>]>>;
1608
1664
  flash: z.ZodOptional<z.ZodObject<{
1609
1665
  consume: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1610
1666
  publish: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -1641,6 +1697,8 @@ export declare const routeDefinitionSchema: z.ZodObject<{
1641
1697
  }[] | undefined;
1642
1698
  }>>;
1643
1699
  form: z.ZodOptional<z.ZodObject<{
1700
+ /** The form's id, which the page's loader reads its state by; the route's name by default. */
1701
+ id: z.ZodOptional<z.ZodString>;
1644
1702
  contentType: z.ZodOptional<z.ZodEnum<["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]>>;
1645
1703
  csrf: z.ZodOptional<z.ZodBoolean>;
1646
1704
  session: z.ZodOptional<z.ZodObject<{
@@ -1689,6 +1747,7 @@ export declare const routeDefinitionSchema: z.ZodObject<{
1689
1747
  }[] | undefined;
1690
1748
  }>>;
1691
1749
  }, "strict", z.ZodTypeAny, {
1750
+ id?: string | undefined;
1692
1751
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
1693
1752
  csrf?: boolean | undefined;
1694
1753
  session?: {
@@ -1705,6 +1764,7 @@ export declare const routeDefinitionSchema: z.ZodObject<{
1705
1764
  }[] | undefined;
1706
1765
  } | undefined;
1707
1766
  }, {
1767
+ id?: string | undefined;
1708
1768
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
1709
1769
  csrf?: boolean | undefined;
1710
1770
  session?: {
@@ -2308,7 +2368,9 @@ export declare const routeDefinitionSchema: z.ZodObject<{
2308
2368
  mode?: "optional" | "required" | undefined;
2309
2369
  write?: boolean | undefined;
2310
2370
  } | undefined;
2311
- auth?: "required" | undefined;
2371
+ auth?: "required" | {
2372
+ role: string;
2373
+ } | undefined;
2312
2374
  flash?: {
2313
2375
  consume?: string[] | undefined;
2314
2376
  publish?: {
@@ -2319,6 +2381,7 @@ export declare const routeDefinitionSchema: z.ZodObject<{
2319
2381
  }[] | undefined;
2320
2382
  } | undefined;
2321
2383
  form?: {
2384
+ id?: string | undefined;
2322
2385
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
2323
2386
  csrf?: boolean | undefined;
2324
2387
  session?: {
@@ -2472,7 +2535,9 @@ export declare const routeDefinitionSchema: z.ZodObject<{
2472
2535
  mode?: "optional" | "required" | undefined;
2473
2536
  write?: boolean | undefined;
2474
2537
  } | undefined;
2475
- auth?: "required" | undefined;
2538
+ auth?: "required" | {
2539
+ role: string;
2540
+ } | undefined;
2476
2541
  flash?: {
2477
2542
  consume?: string[] | undefined;
2478
2543
  publish?: {
@@ -2483,6 +2548,7 @@ export declare const routeDefinitionSchema: z.ZodObject<{
2483
2548
  }[] | undefined;
2484
2549
  } | undefined;
2485
2550
  form?: {
2551
+ id?: string | undefined;
2486
2552
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
2487
2553
  csrf?: boolean | undefined;
2488
2554
  session?: {
@@ -2703,8 +2769,14 @@ export declare const viewDefinitionSchema: z.ZodObject<{
2703
2769
  summary: z.ZodOptional<z.ZodString>;
2704
2770
  description: z.ZodOptional<z.ZodString>;
2705
2771
  tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
2706
- /** `required`: only a signed-in user sees the page; others are sent to sign in. */
2707
- auth: z.ZodOptional<z.ZodEnum<["required"]>>;
2772
+ /** Who sees the page: see `accessRequirementSchema`. */
2773
+ auth: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"required">, z.ZodObject<{
2774
+ role: z.ZodString;
2775
+ }, "strict", z.ZodTypeAny, {
2776
+ role: string;
2777
+ }, {
2778
+ role: string;
2779
+ }>]>>;
2708
2780
  params: z.ZodOptional<z.ZodObject<{
2709
2781
  kind: z.ZodDefault<z.ZodEnum<["zod", "json-schema", "ts-rest"]>>;
2710
2782
  name: z.ZodString;
@@ -2747,7 +2819,9 @@ export declare const viewDefinitionSchema: z.ZodObject<{
2747
2819
  summary?: string | undefined;
2748
2820
  description?: string | undefined;
2749
2821
  tags?: string[] | undefined;
2750
- auth?: "required" | undefined;
2822
+ auth?: "required" | {
2823
+ role: string;
2824
+ } | undefined;
2751
2825
  params?: {
2752
2826
  kind: "json-schema" | "ts-rest" | "zod";
2753
2827
  name: string;
@@ -2770,7 +2844,9 @@ export declare const viewDefinitionSchema: z.ZodObject<{
2770
2844
  summary?: string | undefined;
2771
2845
  description?: string | undefined;
2772
2846
  tags?: string[] | undefined;
2773
- auth?: "required" | undefined;
2847
+ auth?: "required" | {
2848
+ role: string;
2849
+ } | undefined;
2774
2850
  params?: {
2775
2851
  kind?: "json-schema" | "ts-rest" | "zod" | undefined;
2776
2852
  name: string;
@@ -2791,7 +2867,9 @@ export type ViewDefinition = z.infer<typeof viewDefinitionSchema>;
2791
2867
  export type ViewLoaderContext<TContext extends SSRContext, TParams extends z.ZodTypeAny | undefined> = TContext & {
2792
2868
  readonly params: InferOrNever<TParams>;
2793
2869
  };
2794
- export type ViewLoader<TContext extends SSRContext, TParams extends z.ZodTypeAny | undefined, TData extends z.ZodTypeAny> = (context: ViewLoaderContext<TContext, TParams>) => Promise<z.infer<TData>> | z.infer<TData>;
2870
+ /** What a loader returns: the page's data, less the `flash` the framework adds. */
2871
+ export type ViewLoaderData<TData extends z.ZodTypeAny> = z.input<TData> extends Record<string, unknown> ? Omit<z.input<TData>, 'flash'> : z.input<TData>;
2872
+ export type ViewLoader<TContext extends SSRContext, TParams extends z.ZodTypeAny | undefined, TData extends z.ZodTypeAny> = (context: ViewLoaderContext<TContext, TParams>) => Promise<ViewLoaderData<TData>> | ViewLoaderData<TData>;
2795
2873
  export interface ViewSpec<TContext extends SSRContext = SSRContext, TParams extends z.ZodTypeAny | undefined = undefined, TData extends z.ZodTypeAny = z.ZodTypeAny> {
2796
2874
  readonly definition: ViewDefinition;
2797
2875
  readonly params?: TParams;
@@ -2912,8 +2990,14 @@ export declare const moduleManifestSchema: z.ZodObject<{
2912
2990
  mode?: "optional" | "required" | undefined;
2913
2991
  write?: boolean | undefined;
2914
2992
  }>>;
2915
- /** `required`: only a signed-in user reaches the route; others are sent to sign in. */
2916
- auth: z.ZodOptional<z.ZodEnum<["required"]>>;
2993
+ /** Who reaches the route: see `accessRequirementSchema`. */
2994
+ auth: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"required">, z.ZodObject<{
2995
+ role: z.ZodString;
2996
+ }, "strict", z.ZodTypeAny, {
2997
+ role: string;
2998
+ }, {
2999
+ role: string;
3000
+ }>]>>;
2917
3001
  flash: z.ZodOptional<z.ZodObject<{
2918
3002
  consume: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
2919
3003
  publish: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -2950,6 +3034,8 @@ export declare const moduleManifestSchema: z.ZodObject<{
2950
3034
  }[] | undefined;
2951
3035
  }>>;
2952
3036
  form: z.ZodOptional<z.ZodObject<{
3037
+ /** The form's id, which the page's loader reads its state by; the route's name by default. */
3038
+ id: z.ZodOptional<z.ZodString>;
2953
3039
  contentType: z.ZodOptional<z.ZodEnum<["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]>>;
2954
3040
  csrf: z.ZodOptional<z.ZodBoolean>;
2955
3041
  session: z.ZodOptional<z.ZodObject<{
@@ -2998,6 +3084,7 @@ export declare const moduleManifestSchema: z.ZodObject<{
2998
3084
  }[] | undefined;
2999
3085
  }>>;
3000
3086
  }, "strict", z.ZodTypeAny, {
3087
+ id?: string | undefined;
3001
3088
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
3002
3089
  csrf?: boolean | undefined;
3003
3090
  session?: {
@@ -3014,6 +3101,7 @@ export declare const moduleManifestSchema: z.ZodObject<{
3014
3101
  }[] | undefined;
3015
3102
  } | undefined;
3016
3103
  }, {
3104
+ id?: string | undefined;
3017
3105
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
3018
3106
  csrf?: boolean | undefined;
3019
3107
  session?: {
@@ -3617,7 +3705,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
3617
3705
  mode?: "optional" | "required" | undefined;
3618
3706
  write?: boolean | undefined;
3619
3707
  } | undefined;
3620
- auth?: "required" | undefined;
3708
+ auth?: "required" | {
3709
+ role: string;
3710
+ } | undefined;
3621
3711
  flash?: {
3622
3712
  consume?: string[] | undefined;
3623
3713
  publish?: {
@@ -3628,6 +3718,7 @@ export declare const moduleManifestSchema: z.ZodObject<{
3628
3718
  }[] | undefined;
3629
3719
  } | undefined;
3630
3720
  form?: {
3721
+ id?: string | undefined;
3631
3722
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
3632
3723
  csrf?: boolean | undefined;
3633
3724
  session?: {
@@ -3781,7 +3872,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
3781
3872
  mode?: "optional" | "required" | undefined;
3782
3873
  write?: boolean | undefined;
3783
3874
  } | undefined;
3784
- auth?: "required" | undefined;
3875
+ auth?: "required" | {
3876
+ role: string;
3877
+ } | undefined;
3785
3878
  flash?: {
3786
3879
  consume?: string[] | undefined;
3787
3880
  publish?: {
@@ -3792,6 +3885,7 @@ export declare const moduleManifestSchema: z.ZodObject<{
3792
3885
  }[] | undefined;
3793
3886
  } | undefined;
3794
3887
  form?: {
3888
+ id?: string | undefined;
3795
3889
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
3796
3890
  csrf?: boolean | undefined;
3797
3891
  session?: {
@@ -3938,8 +4032,14 @@ export declare const moduleManifestSchema: z.ZodObject<{
3938
4032
  summary: z.ZodOptional<z.ZodString>;
3939
4033
  description: z.ZodOptional<z.ZodString>;
3940
4034
  tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
3941
- /** `required`: only a signed-in user sees the page; others are sent to sign in. */
3942
- auth: z.ZodOptional<z.ZodEnum<["required"]>>;
4035
+ /** Who sees the page: see `accessRequirementSchema`. */
4036
+ auth: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"required">, z.ZodObject<{
4037
+ role: z.ZodString;
4038
+ }, "strict", z.ZodTypeAny, {
4039
+ role: string;
4040
+ }, {
4041
+ role: string;
4042
+ }>]>>;
3943
4043
  params: z.ZodOptional<z.ZodObject<{
3944
4044
  kind: z.ZodDefault<z.ZodEnum<["zod", "json-schema", "ts-rest"]>>;
3945
4045
  name: z.ZodString;
@@ -3982,7 +4082,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
3982
4082
  summary?: string | undefined;
3983
4083
  description?: string | undefined;
3984
4084
  tags?: string[] | undefined;
3985
- auth?: "required" | undefined;
4085
+ auth?: "required" | {
4086
+ role: string;
4087
+ } | undefined;
3986
4088
  params?: {
3987
4089
  kind: "json-schema" | "ts-rest" | "zod";
3988
4090
  name: string;
@@ -4005,7 +4107,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
4005
4107
  summary?: string | undefined;
4006
4108
  description?: string | undefined;
4007
4109
  tags?: string[] | undefined;
4008
- auth?: "required" | undefined;
4110
+ auth?: "required" | {
4111
+ role: string;
4112
+ } | undefined;
4009
4113
  params?: {
4010
4114
  kind?: "json-schema" | "ts-rest" | "zod" | undefined;
4011
4115
  name: string;
@@ -4112,7 +4216,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
4112
4216
  mode?: "optional" | "required" | undefined;
4113
4217
  write?: boolean | undefined;
4114
4218
  } | undefined;
4115
- auth?: "required" | undefined;
4219
+ auth?: "required" | {
4220
+ role: string;
4221
+ } | undefined;
4116
4222
  flash?: {
4117
4223
  consume?: string[] | undefined;
4118
4224
  publish?: {
@@ -4123,6 +4229,7 @@ export declare const moduleManifestSchema: z.ZodObject<{
4123
4229
  }[] | undefined;
4124
4230
  } | undefined;
4125
4231
  form?: {
4232
+ id?: string | undefined;
4126
4233
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
4127
4234
  csrf?: boolean | undefined;
4128
4235
  session?: {
@@ -4269,7 +4376,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
4269
4376
  summary?: string | undefined;
4270
4377
  description?: string | undefined;
4271
4378
  tags?: string[] | undefined;
4272
- auth?: "required" | undefined;
4379
+ auth?: "required" | {
4380
+ role: string;
4381
+ } | undefined;
4273
4382
  params?: {
4274
4383
  kind: "json-schema" | "ts-rest" | "zod";
4275
4384
  name: string;
@@ -4336,7 +4445,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
4336
4445
  mode?: "optional" | "required" | undefined;
4337
4446
  write?: boolean | undefined;
4338
4447
  } | undefined;
4339
- auth?: "required" | undefined;
4448
+ auth?: "required" | {
4449
+ role: string;
4450
+ } | undefined;
4340
4451
  flash?: {
4341
4452
  consume?: string[] | undefined;
4342
4453
  publish?: {
@@ -4347,6 +4458,7 @@ export declare const moduleManifestSchema: z.ZodObject<{
4347
4458
  }[] | undefined;
4348
4459
  } | undefined;
4349
4460
  form?: {
4461
+ id?: string | undefined;
4350
4462
  contentType?: "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain" | undefined;
4351
4463
  csrf?: boolean | undefined;
4352
4464
  session?: {
@@ -4493,7 +4605,9 @@ export declare const moduleManifestSchema: z.ZodObject<{
4493
4605
  summary?: string | undefined;
4494
4606
  description?: string | undefined;
4495
4607
  tags?: string[] | undefined;
4496
- auth?: "required" | undefined;
4608
+ auth?: "required" | {
4609
+ role: string;
4610
+ } | undefined;
4497
4611
  params?: {
4498
4612
  kind?: "json-schema" | "ts-rest" | "zod" | undefined;
4499
4613
  name: string;
@@ -4567,6 +4681,6 @@ export { fromTsRestRoute, fromTsRestRouter } from './adapters/ts-rest.js';
4567
4681
  export type { FromTsRestRouteOptions, FromTsRestRouterOptions, RouterRouteConfig, } from './adapters/ts-rest.js';
4568
4682
  export { RENDER_PROGRAM_FILE, RENDER_PROGRAM_VERSION, renderFlashMessageSchema, renderFlashSchema, renderNodeSchema, renderPathSchema, renderProgramSchema, renderSourceLocationSchema, } from './render-program.js';
4569
4683
  export type { RenderFlashMessage, RenderNode, RenderPath, RenderProgram, RenderSourceLocation, } from './render-program.js';
4570
- export { executeRenderProgram, programUsesCsrf, readRenderProgram, RENDER_CSRF_FIELD, RenderProgramError, prepareViewData, schemaDeclaresField, } from './render-execute.js';
4684
+ export { executeRenderProgram, programUsesCsrf, readRenderProgram, RENDER_CSRF_FIELD, RenderProgramError, prepareViewData, schemaDeclaresField, withShellData, } from './render-execute.js';
4571
4685
  export type { ExecuteRenderProgramOptions, PreparedViewData } from './render-execute.js';
4572
- export { CLIENT_NAV_HEADERS, CLIENT_NAV_SUBMISSION_FIELD } from './client-nav.js';
4686
+ export { CLIENT_NAV_HEADERS, CLIENT_NAV_SUBMISSION_FIELD, FORM_ID_FIELD, FORM_PAGE_FIELD, } from './client-nav.js';
package/dist/index.js CHANGED
@@ -4,6 +4,35 @@ export const CONTRACT_VERSION = '1.0.0';
4
4
  export const contractVersionLiteral = z.literal(CONTRACT_VERSION);
5
5
  export const moduleKindSchema = z.enum(['frontend', 'backend']);
6
6
  export const logLevelSchema = z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
7
+ /**
8
+ * What a page binds for one form: whether it came back from a failed submission, what was typed in
9
+ * each field, and the first error per field (and `form`, for errors about the whole form).
10
+ */
11
+ export function formStateSchema(fields) {
12
+ const strings = (names) => z.object(Object.fromEntries(names.map((name) => [name, z.string().optional()])));
13
+ return z.object({
14
+ submitted: z.boolean(),
15
+ values: strings(fields),
16
+ errors: strings([...fields, 'form']),
17
+ });
18
+ }
19
+ /** A form's state as `formStateSchema(fields)` binds it, with `defaults` for a fresh form. */
20
+ export function formView(state, fields, defaults = {}) {
21
+ const values = {};
22
+ const errors = {};
23
+ for (const field of fields) {
24
+ const value = state.values[field];
25
+ const typed = typeof value === 'string' ? value : Array.isArray(value) ? value[0] : undefined;
26
+ const shown = state.submitted ? typed : defaults[field];
27
+ if (shown !== undefined)
28
+ values[field] = shown;
29
+ if (state.errors[field])
30
+ errors[field] = state.errors[field];
31
+ }
32
+ if (state.errors.form)
33
+ errors.form = state.errors.form;
34
+ return { submitted: state.submitted, values, errors };
35
+ }
7
36
  export const moduleErrorCodeSchema = z.enum([
8
37
  'validation',
9
38
  'auth',
@@ -19,6 +48,14 @@ export const moduleErrorSchema = z.object({
19
48
  cause: z.unknown().optional(),
20
49
  correlationId: z.string().optional(),
21
50
  });
51
+ /**
52
+ * Who reaches a route or view: `'required'` is anyone signed in; `{ role }` is someone signed in
53
+ * whose user has that role. Others are sent to sign in; signed in without the role is a 404.
54
+ */
55
+ export const accessRequirementSchema = z.union([
56
+ z.literal('required'),
57
+ z.object({ role: z.string().min(1) }).strict(),
58
+ ]);
22
59
  export const httpMethodSchema = z.enum([
23
60
  'GET',
24
61
  'HEAD',
@@ -79,6 +116,8 @@ export const routeSessionSchema = z
79
116
  })
80
117
  .strict();
81
118
  export const flashLevelSchema = z.enum(['info', 'success', 'warning', 'error']);
119
+ /** What a page binds as `flash`: the messages an action left for it. The framework supplies it. */
120
+ export const flashSchema = z.array(z.object({ level: flashLevelSchema, message: z.string() }));
82
121
  export const flashPublishConditionSchema = z.enum(['always', 'success', 'error']);
83
122
  export const routeFlashMessageSchema = z
84
123
  .object({
@@ -96,6 +135,8 @@ export const routeFlashSchema = z
96
135
  .strict();
97
136
  export const routeFormSchema = z
98
137
  .object({
138
+ /** The form's id, which the page's loader reads its state by; the route's name by default. */
139
+ id: z.string().min(1).optional(),
99
140
  contentType: formEncodingSchema.optional(),
100
141
  csrf: z.boolean().optional(),
101
142
  session: routeSessionSchema.optional(),
@@ -159,8 +200,8 @@ export const routeDefinitionSchema = z.object({
159
200
  interaction: routeInteractionKindSchema.optional(),
160
201
  requestHooks: z.array(requestHookReferenceSchema).optional(),
161
202
  session: routeSessionSchema.optional(),
162
- /** `required`: only a signed-in user reaches the route; others are sent to sign in. */
163
- auth: z.enum(['required']).optional(),
203
+ /** Who reaches the route: see `accessRequirementSchema`. */
204
+ auth: accessRequirementSchema.optional(),
164
205
  flash: routeFlashSchema.optional(),
165
206
  form: routeFormSchema.optional(),
166
207
  fragment: routeFragmentSchema.optional(),
@@ -182,8 +223,8 @@ export const viewDefinitionSchema = z.object({
182
223
  summary: z.string().optional(),
183
224
  description: z.string().optional(),
184
225
  tags: z.array(z.string()).optional(),
185
- /** `required`: only a signed-in user sees the page; others are sent to sign in. */
186
- auth: z.enum(['required']).optional(),
226
+ /** Who sees the page: see `accessRequirementSchema`. */
227
+ auth: accessRequirementSchema.optional(),
187
228
  params: schemaReferenceSchema.optional(),
188
229
  data: schemaReferenceSchema.optional(),
189
230
  renderMode: z.enum(['ssg', 'ssr', 'spa']).optional(),
@@ -237,5 +278,5 @@ export function createModule(definition) {
237
278
  }
238
279
  export { fromTsRestRoute, fromTsRestRouter } from './adapters/ts-rest.js';
239
280
  export { RENDER_PROGRAM_FILE, RENDER_PROGRAM_VERSION, renderFlashMessageSchema, renderFlashSchema, renderNodeSchema, renderPathSchema, renderProgramSchema, renderSourceLocationSchema, } from './render-program.js';
240
- export { executeRenderProgram, programUsesCsrf, readRenderProgram, RENDER_CSRF_FIELD, RenderProgramError, prepareViewData, schemaDeclaresField, } from './render-execute.js';
241
- export { CLIENT_NAV_HEADERS, CLIENT_NAV_SUBMISSION_FIELD } from './client-nav.js';
281
+ export { executeRenderProgram, programUsesCsrf, readRenderProgram, RENDER_CSRF_FIELD, RenderProgramError, prepareViewData, schemaDeclaresField, withShellData, } from './render-execute.js';
282
+ export { CLIENT_NAV_HEADERS, CLIENT_NAV_SUBMISSION_FIELD, FORM_ID_FIELD, FORM_PAGE_FIELD, } from './client-nav.js';
@@ -10,6 +10,13 @@ export interface ExecuteRenderProgramOptions {
10
10
  * POST forms render without a token field.
11
11
  */
12
12
  readonly csrfToken?: string | false;
13
+ /**
14
+ * A new id for each POST form rendered, so the same submission sent again (a resent post, a
15
+ * double click) is answered once. Left out, forms render without one.
16
+ */
17
+ readonly submissionId?: () => string;
18
+ /** The page's own address, rendered into each POST form so a failure returns to it. */
19
+ readonly pagePath?: string;
13
20
  }
14
21
  export declare function readRenderProgram(value: unknown, source: string): RenderProgram;
15
22
  export declare function programUsesCsrf(program: RenderProgram): boolean;
@@ -28,6 +35,11 @@ export type PreparedViewData = {
28
35
  * failing that, without it, starting with what the schema declares.
29
36
  */
30
37
  export declare function prepareViewData(schema: unknown, loaded: unknown, flash: readonly unknown[]): PreparedViewData;
38
+ /**
39
+ * A page's data with the app's shell data beside it as `shell`, checked against the shell's schema
40
+ * when it has one.
41
+ */
42
+ export declare function withShellData(data: unknown, shellSchema: unknown, loaded: unknown): PreparedViewData;
31
43
  /**
32
44
  * Whether a zod-style schema's underlying object declares `field`, looking through wrappers
33
45
  * such as optional, default, refine and transform, and into unions and intersections. A schema
@@ -1,3 +1,4 @@
1
+ import { CLIENT_NAV_SUBMISSION_FIELD, FORM_PAGE_FIELD } from './client-nav.js';
1
2
  import { RENDER_PROGRAM_VERSION } from './render-version.js';
2
3
  export const RENDER_CSRF_FIELD = '_csrf';
3
4
  const SAFE_URL_SCHEMES = new Set(['http', 'https', 'mailto', 'tel']);
@@ -63,6 +64,12 @@ function run(nodes, data, scopes, options, out) {
63
64
  throw new Error('A POST form needs a CSRF token, but none was supplied.');
64
65
  }
65
66
  out.push(`<input type="hidden" name="${RENDER_CSRF_FIELD}" value="${escapeAttribute(options.csrfToken)}">`);
67
+ if (options.submissionId) {
68
+ out.push(`<input type="hidden" name="${CLIENT_NAV_SUBMISSION_FIELD}" value="${escapeAttribute(options.submissionId())}">`);
69
+ }
70
+ if (options.pagePath) {
71
+ out.push(`<input type="hidden" name="${FORM_PAGE_FIELD}" value="${escapeAttribute(options.pagePath)}">`);
72
+ }
66
73
  break;
67
74
  case 'text': {
68
75
  const value = read(node.path, data, scopes);
@@ -194,6 +201,22 @@ export function prepareViewData(schema, loaded, flash) {
194
201
  }
195
202
  return { ok: false, error: describeSchemaError(firstError) };
196
203
  }
204
+ /**
205
+ * A page's data with the app's shell data beside it as `shell`, checked against the shell's schema
206
+ * when it has one.
207
+ */
208
+ export function withShellData(data, shellSchema, loaded) {
209
+ if (!isPlainObject(data)) {
210
+ return { ok: true, data };
211
+ }
212
+ if (!shellSchema || typeof shellSchema.safeParse !== 'function') {
213
+ return { ok: true, data: { ...data, shell: loaded } };
214
+ }
215
+ const parsed = shellSchema.safeParse(loaded);
216
+ return parsed.success
217
+ ? { ok: true, data: { ...data, shell: parsed.data } }
218
+ : { ok: false, error: describeSchemaError(parsed.error) };
219
+ }
197
220
  function mergeFlash(value, flash) {
198
221
  if (!isPlainObject(value)) {
199
222
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstir-io/module-contract",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Public contract definitions for Webstir frontend and backend module providers.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -147,9 +147,24 @@
147
147
  "additionalProperties": false
148
148
  },
149
149
  "auth": {
150
- "type": "string",
151
- "enum": [
152
- "required"
150
+ "anyOf": [
151
+ {
152
+ "type": "string",
153
+ "const": "required"
154
+ },
155
+ {
156
+ "type": "object",
157
+ "properties": {
158
+ "role": {
159
+ "type": "string",
160
+ "minLength": 1
161
+ }
162
+ },
163
+ "required": [
164
+ "role"
165
+ ],
166
+ "additionalProperties": false
167
+ }
153
168
  ]
154
169
  },
155
170
  "flash": {
@@ -205,6 +220,10 @@
205
220
  "form": {
206
221
  "type": "object",
207
222
  "properties": {
223
+ "id": {
224
+ "type": "string",
225
+ "minLength": 1
226
+ },
208
227
  "contentType": {
209
228
  "type": "string",
210
229
  "enum": [
@@ -486,10 +505,7 @@
486
505
  }
487
506
  },
488
507
  "auth": {
489
- "type": "string",
490
- "enum": [
491
- "required"
492
- ]
508
+ "$ref": "#/definitions/module-manifest/properties/routes/items/properties/auth"
493
509
  },
494
510
  "params": {
495
511
  "$ref": "#/definitions/module-manifest/properties/routes/items/properties/input/properties/params"
@@ -76,9 +76,24 @@
76
76
  "additionalProperties": false
77
77
  },
78
78
  "auth": {
79
- "type": "string",
80
- "enum": [
81
- "required"
79
+ "anyOf": [
80
+ {
81
+ "type": "string",
82
+ "const": "required"
83
+ },
84
+ {
85
+ "type": "object",
86
+ "properties": {
87
+ "role": {
88
+ "type": "string",
89
+ "minLength": 1
90
+ }
91
+ },
92
+ "required": [
93
+ "role"
94
+ ],
95
+ "additionalProperties": false
96
+ }
82
97
  ]
83
98
  },
84
99
  "flash": {
@@ -134,6 +149,10 @@
134
149
  "form": {
135
150
  "type": "object",
136
151
  "properties": {
152
+ "id": {
153
+ "type": "string",
154
+ "minLength": 1
155
+ },
137
156
  "contentType": {
138
157
  "type": "string",
139
158
  "enum": [
@@ -29,9 +29,24 @@
29
29
  }
30
30
  },
31
31
  "auth": {
32
- "type": "string",
33
- "enum": [
34
- "required"
32
+ "anyOf": [
33
+ {
34
+ "type": "string",
35
+ "const": "required"
36
+ },
37
+ {
38
+ "type": "object",
39
+ "properties": {
40
+ "role": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ }
44
+ },
45
+ "required": [
46
+ "role"
47
+ ],
48
+ "additionalProperties": false
49
+ }
35
50
  ]
36
51
  },
37
52
  "params": {