@nsail/stack 0.0.0-stage → 0.1.135

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.
@@ -0,0 +1,165 @@
1
+ {
2
+ "Common.Submit": "Guardar",
3
+ "Common.Filter": "Buscar",
4
+ "Common.Search": "Escribí para buscar",
5
+ "Common.SearchPrompt": "Buscar {0}",
6
+ "Common.CreateNew": "Crear {0}...",
7
+ "Common.QuickAdd": "Agregar «{0}»",
8
+ "Common.NoOptions": "Todavía no hay {0} para elegir.",
9
+ "Common.Filters": "Filtros",
10
+ "Common.FiltersCount": "Filtros ({0})",
11
+ "Common.New": "Nuevo",
12
+ "Common.Quick": "Rápido",
13
+ "Common.Loading": "Cargando",
14
+ "Common.Menu": "Menú",
15
+ "Common.MoreActions": "Más acciones",
16
+ "Common.ShowOptions": "Mostrar opciones",
17
+ "Common.LoadMore": "Cargar más",
18
+ "Common.SelectPage": "Seleccionar las filas de esta página",
19
+ "Common.SelectRow": "Seleccionar fila",
20
+ "Common.Selected": "{0} seleccionado",
21
+ "Common.Selected.Plural": "{0} seleccionados",
22
+ "Common.SelectAll": "Seleccionar la única fila",
23
+ "Common.SelectAll.Plural": "Seleccionar los {0}",
24
+ "Common.ClearSelection": "Quitar selección",
25
+ "Common.Enabled": "Habilitado",
26
+ "Actions.Add": "Agregar",
27
+ "Actions.Edit": "Editar",
28
+ "Actions.Delete": "Eliminar",
29
+ "Actions.Confirm": "Confirmar",
30
+ "Actions.Cancel": "Cancelar",
31
+ "Actions.Export": "Exportar",
32
+ "Actions.ExportExcel": "Excel",
33
+ "Actions.ExportCsv": "CSV",
34
+ "Actions.ExportPdf": "PDF",
35
+ "Common.Auto": "Automático",
36
+ "Common.All": "Todos",
37
+ "Common.Close": "Salir",
38
+ "Common.SkipToContent": "Saltar al contenido",
39
+ "Common.Help": "Ayuda",
40
+ "Common.Details": "Detalle",
41
+ "Common.Guide": "Ayuda",
42
+ "Common.GuideMissing": "No se pudo leer este capítulo del manual.",
43
+ "Common.Saved": "Guardado",
44
+ "Common.RowsPerPage": "Filas por página",
45
+ "Common.UnsavedChanges": "Tenés cambios sin guardar. ¿Seguro que querés salir?",
46
+ "Common.UnsavedChangesTitle": "Cambios sin guardar",
47
+ "Common.ConfirmSend": "¿Confirmás esta acción?",
48
+ "Common.InUse": "Este registro está en uso y no se puede borrar. Para dejar de verlo, deshabilitalo.",
49
+ "Common.Confirm": "Confirmar",
50
+ "Common.Alert": "Aviso",
51
+ "Common.Yes": "Sí",
52
+ "Common.No": "No",
53
+ "Common.Ok": "Aceptar",
54
+ "Common.Upload": "Subir",
55
+ "Common.File": "Archivo",
56
+ "Common.NoFile": "Ningún archivo elegido",
57
+ "Common.FileTooLarge": "El archivo supera los {0} MB",
58
+ "Common.DateLetters": "dma",
59
+ "Common.Hours": "h",
60
+ "Common.Back": "Atrás",
61
+ "Common.Next": "Siguiente",
62
+ "Common.Finish": "Finalizar",
63
+ "Common.Home": "Inicio",
64
+ "Common.PageNotFound": "Esta página no existe.",
65
+ "Common.NotAuthorized": "No tenés permiso para acceder a esta página. Probá cambiar de organización desde el menú de usuario.",
66
+ "Common.UnhandledError": "Algo se rompió.",
67
+ "Common.ErrorReference": "Referencia {handle}",
68
+ "Common.Reload": "Recargar",
69
+ "Common.Retry": "Reintentar",
70
+ "Common.NewVersion": "Hay una versión nueva.",
71
+
72
+ "Components.NsPasswordField.Stored": "Guardado. Vacío para conservarlo.",
73
+
74
+ "Components.PasswordStrength.Weak": "Débil",
75
+ "Components.PasswordStrength.Medium": "Media",
76
+ "Components.PasswordStrength.Strong": "Fuerte",
77
+
78
+ "Problems.RuleViolation.Title": "No se pudo completar la operación",
79
+ "Problems.InvalidState.Title": "Estado inválido",
80
+ "Problems.InvalidState": "Estado inválido: {state}",
81
+ "Problems.NotFound.Title": "No se encontró el recurso",
82
+ "Problems.NotFound": "No se encontró {entity}",
83
+ "Problems.Conflict.Title": "Conflicto de concurrencia",
84
+ "Problems.Conflict": "Alguien más lo modificó mientras lo editabas",
85
+ "Problems.ConflictAt": "Alguien más lo modificó el {at}, mientras lo editabas",
86
+ "Problems.AlreadyExists.Title": "El recurso ya existe",
87
+ "Problems.AlreadyExists": "Ya existe {entity} con el valor {value}",
88
+ "Problems.InsufficientStock.Title": "Stock insuficiente",
89
+ "Problems.InsufficientStock": "Stock insuficiente de {product}: quedan {available}",
90
+ "Problems.MissingTaxRate.Title": "Falta la alícuota",
91
+ "Problems.MissingTaxRate": "El producto {product} no tiene alícuota asignada",
92
+ "Problems.MissingPrice.Title": "Falta el precio",
93
+ "Problems.MissingPrice": "El producto {product} no tiene precio en la Lista de Venta",
94
+
95
+ "Problems.InvalidModel.Title": "Hay campos con datos inválidos",
96
+ "Problems.Required": "Obligatorio",
97
+ "Problems.InvalidFormat": "Formato inválido",
98
+ "Problems.UnreadableDate": "Esto no es una fecha — escribila como {shape}",
99
+ "Problems.UnreadableAddress": "Esto no es una dirección web — escribila completa, como https://ejemplo.com",
100
+ "Problems.UnreadableAddressDomain": "Esto no es una dirección web — revisá el dominio",
101
+ "Problems.OutOfRange": "Fuera de rango",
102
+ "Problems.MaxLength": "No más de {max} caracteres",
103
+ "Problems.NotFuture": "No puede ser posterior a hoy",
104
+ "Problems.NotNegative": "No puede ser menor que cero",
105
+ "Problems.NotPositive": "No puede ser mayor que cero",
106
+ "Problems.PositiveAmount": "Tiene que ser mayor que cero",
107
+ "Problems.NotAbove": "No puede ser mayor que el límite superior",
108
+ "Problems.Mismatch": "Los dos valores no coinciden",
109
+ "Problems.Invalid": "Valor inválido",
110
+ "Problems.BadRequest": "No se pudo interpretar el pedido",
111
+
112
+ "Problems.Unauthorized.Title": "Falta autenticación",
113
+ "Problems.Unauthorized": "Tenés que iniciar sesión para hacer esto",
114
+ "Problems.Forbidden.Title": "Acceso denegado",
115
+ "Problems.Forbidden": "No tenés permiso para hacer esto",
116
+
117
+ "Problems.TooManyRequests.Title": "Demasiados intentos",
118
+ "Problems.TooManyRequests": "Demasiados intentos. Esperá un momento y probá de nuevo",
119
+
120
+ "Problems.InternalError.Title": "Ocurrió un error inesperado",
121
+ "Problems.InternalError": "Algo falló de nuestro lado",
122
+ "Problems.Unknown.Title": "Ocurrió un error desconocido",
123
+ "Problems.Unknown": "Algo salió mal",
124
+ "Problems.Canceled.Title": "Operación cancelada",
125
+
126
+ "Problems.RequestFailed.Title": "No se pudo contactar al servidor",
127
+ "Problems.NoResponse": "El servidor no respondió",
128
+ "Problems.HttpStatus": "El servidor respondió con un error",
129
+ "Problems.EmptyResponse": "El servidor devolvió una respuesta vacía",
130
+ "Problems.DeserializationFailed": "No se pudo leer la respuesta del servidor",
131
+ "Problems.StatusFallback": "",
132
+
133
+ "Mud.MudDataGridPager_RowsPerPage": "Filas por página:",
134
+ "Mud.MudDataGridPager_InfoFormat": "{0}-{1} de {2}",
135
+ "Mud.MudDataGridPager_AllItems": "Todos",
136
+ "Mud.MudTablePager_FirstPage": "Primera página",
137
+ "Mud.MudTablePager_PreviousPage": "Página anterior",
138
+ "Mud.MudTablePager_NextPage": "Página siguiente",
139
+ "Mud.MudTablePager_LastPage": "Última página",
140
+ "Mud.MudTable_Loading": "Cargando...",
141
+ "Mud.MudTreeView_ExpandItem": "Expandir",
142
+ "Mud.MudTreeView_CollapseItem": "Contraer",
143
+ "Mud.MudNavGroup_ToggleExpand": "Expandir",
144
+ "Mud.MudInput_Clear": "Limpiar",
145
+ "Mud.MudSnackbar_Close": "Cerrar",
146
+ "Mud.MudAlert_Close": "Cerrar",
147
+ "Mud.MudDialog_Close": "Cerrar",
148
+ "Mud.MudChip_Close": "Quitar",
149
+ "Mud.MudBaseDatePicker_Open": "Abrir calendario",
150
+ "Mud.MudBaseDatePicker_PrevMonth": "Mes anterior",
151
+ "Mud.MudBaseDatePicker_NextMonth": "Mes siguiente",
152
+ "Mud.MudBaseDatePicker_PrevYear": "Año anterior",
153
+ "Mud.MudBaseDatePicker_NextYear": "Año siguiente",
154
+ "Mud.MudTimePicker_Open": "Abrir reloj",
155
+ "Common.NoRecords": "No hay registros para mostrar",
156
+ "Common.TimeGridHiddenDay": "Mostrando {day} — 1 día más de esta semana está oculto para entrar",
157
+ "Common.TimeGridHiddenDay.Plural": "Mostrando {day} — {count} días más de esta semana están ocultos para entrar",
158
+
159
+ "Problems.Timeout.Title": "La operación tardó demasiado",
160
+ "Problems.Timeout": "La operación tardó demasiado en completarse",
161
+ "Problems.ExternalTimeout.Title": "Un servicio tardó demasiado en responder",
162
+ "Problems.ExternalServiceUnavailable.Title": "Un servicio no está disponible",
163
+
164
+ "Common.Resource": "Recurso"
165
+ }
@@ -0,0 +1,165 @@
1
+ {
2
+ "Common.Submit": "Save",
3
+ "Common.Filter": "Search",
4
+ "Common.Search": "Type to search",
5
+ "Common.SearchPrompt": "Search {0}",
6
+ "Common.CreateNew": "Create {0}...",
7
+ "Common.QuickAdd": "Add \"{0}\"",
8
+ "Common.NoOptions": "There is no {0} to pick yet.",
9
+ "Common.Filters": "Filters",
10
+ "Common.FiltersCount": "Filters ({0})",
11
+ "Common.New": "New",
12
+ "Common.Quick": "Quick",
13
+ "Common.Loading": "Loading",
14
+ "Common.Menu": "Menu",
15
+ "Common.MoreActions": "More actions",
16
+ "Common.ShowOptions": "Show options",
17
+ "Common.LoadMore": "Load more",
18
+ "Common.SelectPage": "Select the rows on this page",
19
+ "Common.SelectRow": "Select row",
20
+ "Common.Selected": "{0} selected",
21
+ "Common.Selected.Plural": "{0} selected",
22
+ "Common.SelectAll": "Select the {0} row",
23
+ "Common.SelectAll.Plural": "Select all {0}",
24
+ "Common.ClearSelection": "Clear selection",
25
+ "Common.Enabled": "Enabled",
26
+ "Actions.Add": "Add",
27
+ "Actions.Edit": "Edit",
28
+ "Actions.Delete": "Delete",
29
+ "Actions.Confirm": "Confirm",
30
+ "Actions.Cancel": "Cancel",
31
+ "Actions.Export": "Export",
32
+ "Actions.ExportExcel": "Excel",
33
+ "Actions.ExportCsv": "CSV",
34
+ "Actions.ExportPdf": "PDF",
35
+ "Common.Auto": "Auto",
36
+ "Common.All": "All",
37
+ "Common.Close": "Close",
38
+ "Common.SkipToContent": "Skip to content",
39
+ "Common.Help": "Help",
40
+ "Common.Details": "Details",
41
+ "Common.Guide": "Guide",
42
+ "Common.GuideMissing": "This chapter of the guide could not be read.",
43
+ "Common.Saved": "Saved",
44
+ "Common.RowsPerPage": "Rows per page",
45
+ "Common.UnsavedChanges": "You have unsaved changes. Are you sure you want to leave?",
46
+ "Common.UnsavedChangesTitle": "Unsaved changes",
47
+ "Common.ConfirmSend": "Confirm this action?",
48
+ "Common.InUse": "This record is in use and cannot be deleted. To stop seeing it, disable it.",
49
+ "Common.Confirm": "Confirm",
50
+ "Common.Alert": "Alert",
51
+ "Common.Yes": "Yes",
52
+ "Common.No": "No",
53
+ "Common.Ok": "OK",
54
+ "Common.Upload": "Upload",
55
+ "Common.File": "File",
56
+ "Common.NoFile": "No file chosen",
57
+ "Common.FileTooLarge": "The file is larger than {0} MB",
58
+ "Common.DateLetters": "dmy",
59
+ "Common.Hours": "h",
60
+ "Common.Back": "Back",
61
+ "Common.Next": "Next",
62
+ "Common.Finish": "Finish",
63
+ "Common.Home": "Home",
64
+ "Common.PageNotFound": "This page does not exist.",
65
+ "Common.NotAuthorized": "You do not have permission to access this page. Try changing your organization from the user menu.",
66
+ "Common.UnhandledError": "Something broke.",
67
+ "Common.ErrorReference": "Reference {handle}",
68
+ "Common.Reload": "Reload",
69
+ "Common.Retry": "Retry",
70
+ "Common.NewVersion": "A new version is available.",
71
+
72
+ "Components.NsPasswordField.Stored": "Stored. Leave this empty to keep it.",
73
+
74
+ "Components.PasswordStrength.Weak": "Weak",
75
+ "Components.PasswordStrength.Medium": "Medium",
76
+ "Components.PasswordStrength.Strong": "Strong",
77
+
78
+ "Problems.RuleViolation.Title": "The operation could not be completed",
79
+ "Problems.InvalidState.Title": "Invalid state",
80
+ "Problems.InvalidState": "Invalid state: {state}",
81
+ "Problems.NotFound.Title": "Resource not found",
82
+ "Problems.NotFound": "{entity} was not found",
83
+ "Problems.Conflict.Title": "Concurrency conflict",
84
+ "Problems.Conflict": "Someone else changed this while you were editing it",
85
+ "Problems.ConflictAt": "Someone else changed this on {at}, while you were editing it",
86
+ "Problems.AlreadyExists.Title": "Resource already exists",
87
+ "Problems.AlreadyExists": "{entity} with value {value} already exists",
88
+ "Problems.InsufficientStock.Title": "Insufficient stock",
89
+ "Problems.InsufficientStock": "Insufficient stock of {product}: {available} available",
90
+ "Problems.MissingTaxRate.Title": "Missing tax rate",
91
+ "Problems.MissingTaxRate": "{product} has no tax rate assigned",
92
+ "Problems.MissingPrice.Title": "Missing price",
93
+ "Problems.MissingPrice": "{product} has no price the selling list can resolve",
94
+
95
+ "Problems.InvalidModel.Title": "One or more fields contain invalid data",
96
+ "Problems.Required": "Required",
97
+ "Problems.InvalidFormat": "Invalid format",
98
+ "Problems.UnreadableDate": "This is not a date — write it as {shape}",
99
+ "Problems.UnreadableAddress": "This is not a web address — write it whole, like https://example.com",
100
+ "Problems.UnreadableAddressDomain": "This is not a web address — check the domain",
101
+ "Problems.OutOfRange": "Out of range",
102
+ "Problems.MaxLength": "No longer than {max} characters",
103
+ "Problems.NotFuture": "Cannot be later than today",
104
+ "Problems.NotNegative": "Cannot be below zero",
105
+ "Problems.NotPositive": "Cannot be above zero",
106
+ "Problems.PositiveAmount": "Must be above zero",
107
+ "Problems.NotAbove": "Cannot be greater than the upper bound",
108
+ "Problems.Mismatch": "The two values do not match",
109
+ "Problems.Invalid": "Invalid value",
110
+ "Problems.BadRequest": "The request could not be understood",
111
+
112
+ "Problems.Unauthorized.Title": "Authentication required",
113
+ "Problems.Unauthorized": "You need to sign in to do this",
114
+ "Problems.Forbidden.Title": "Access denied",
115
+ "Problems.Forbidden": "You do not have permission to do this",
116
+
117
+ "Problems.TooManyRequests.Title": "Too many requests",
118
+ "Problems.TooManyRequests": "Too many attempts. Wait a moment and try again",
119
+
120
+ "Problems.InternalError.Title": "An unexpected error occurred",
121
+ "Problems.InternalError": "Something went wrong on our side",
122
+ "Problems.Unknown.Title": "An unknown error occurred",
123
+ "Problems.Unknown": "Something went wrong",
124
+ "Problems.Canceled.Title": "Operation canceled",
125
+
126
+ "Problems.RequestFailed.Title": "Unable to contact the server",
127
+ "Problems.NoResponse": "The server did not respond",
128
+ "Problems.HttpStatus": "The server responded with an error",
129
+ "Problems.EmptyResponse": "The server sent an empty response",
130
+ "Problems.DeserializationFailed": "The server response could not be read",
131
+ "Problems.StatusFallback": "",
132
+
133
+ "Mud.MudDataGridPager_RowsPerPage": "Rows per page:",
134
+ "Mud.MudDataGridPager_InfoFormat": "{0}-{1} of {2}",
135
+ "Mud.MudDataGridPager_AllItems": "All",
136
+ "Mud.MudTablePager_FirstPage": "First page",
137
+ "Mud.MudTablePager_PreviousPage": "Previous page",
138
+ "Mud.MudTablePager_NextPage": "Next page",
139
+ "Mud.MudTablePager_LastPage": "Last page",
140
+ "Mud.MudTable_Loading": "Loading...",
141
+ "Mud.MudTreeView_ExpandItem": "Expand",
142
+ "Mud.MudTreeView_CollapseItem": "Collapse",
143
+ "Mud.MudNavGroup_ToggleExpand": "Toggle",
144
+ "Mud.MudInput_Clear": "Clear",
145
+ "Mud.MudSnackbar_Close": "Close",
146
+ "Mud.MudAlert_Close": "Close",
147
+ "Mud.MudDialog_Close": "Close",
148
+ "Mud.MudChip_Close": "Remove",
149
+ "Mud.MudBaseDatePicker_Open": "Open calendar",
150
+ "Mud.MudBaseDatePicker_PrevMonth": "Previous month",
151
+ "Mud.MudBaseDatePicker_NextMonth": "Next month",
152
+ "Mud.MudBaseDatePicker_PrevYear": "Previous year",
153
+ "Mud.MudBaseDatePicker_NextYear": "Next year",
154
+ "Mud.MudTimePicker_Open": "Open clock",
155
+ "Common.NoRecords": "No records to show",
156
+ "Common.TimeGridHiddenDay": "Showing {day} — 1 more day this week is hidden to fit",
157
+ "Common.TimeGridHiddenDay.Plural": "Showing {day} — {count} more days this week are hidden to fit",
158
+
159
+ "Problems.Timeout.Title": "The operation took too long",
160
+ "Problems.Timeout": "The operation took too long to complete",
161
+ "Problems.ExternalTimeout.Title": "A service took too long to respond",
162
+ "Problems.ExternalServiceUnavailable.Title": "A service is unavailable",
163
+
164
+ "Common.Resource": "Resource"
165
+ }
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Mediator } from "./mediator";
3
+ import { Strings } from "./strings";
4
+ export interface StackProviderProps {
5
+ mediator: Mediator;
6
+ strings: Strings;
7
+ children: ReactNode;
8
+ }
9
+ /** What every hook of the stack reads: the mediator that sends and the strings that speak. */
10
+ export declare function StackProvider({ mediator, strings, children }: StackProviderProps): import("react").JSX.Element;
11
+ export interface Catalog {
12
+ strings: Strings | null;
13
+ failed: boolean;
14
+ }
15
+ /** The catalog for a language: the Stack's sentences with the app's merged over them — the app's
16
+ * keys win, as an app's strings registered last win on the server. A failed read still yields
17
+ * the Stack's own, so the page renders keyed rather than blank. */
18
+ export declare function useCatalog(language: string, load?: (language: string) => Promise<Record<string, string>>): Catalog;
19
+ export declare function useMediator(): Mediator;
20
+ export declare function useStrings(): Strings;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
4
+ import { createContext, useContext, useEffect, useState } from "react";
5
+ import { Strings, stackStrings } from "./strings";
6
+ const MediatorContext = createContext(null);
7
+ const StringsContext = createContext(null);
8
+ /** What every hook of the stack reads: the mediator that sends and the strings that speak. */
9
+ export function StackProvider({ mediator, strings, children }) {
10
+ return (_jsx(MediatorContext.Provider, { value: mediator, children: _jsx(StringsContext.Provider, { value: strings, children: children }) }));
11
+ }
12
+ /** The catalog for a language: the Stack's sentences with the app's merged over them — the app's
13
+ * keys win, as an app's strings registered last win on the server. A failed read still yields
14
+ * the Stack's own, so the page renders keyed rather than blank. */
15
+ export function useCatalog(language, load) {
16
+ const [catalog, setCatalog] = useState({ strings: null, failed: false });
17
+ useEffect(() => {
18
+ let live = true;
19
+ (load ? load(language) : Promise.resolve({}))
20
+ .then((entries) => {
21
+ if (live) {
22
+ document.documentElement.lang = language;
23
+ setCatalog({ strings: new Strings(language, { ...stackStrings(language), ...entries }), failed: false });
24
+ }
25
+ })
26
+ .catch(() => {
27
+ if (live) {
28
+ setCatalog({ strings: new Strings(language, stackStrings(language)), failed: true });
29
+ }
30
+ });
31
+ return () => {
32
+ live = false;
33
+ };
34
+ }, [language, load]);
35
+ return catalog;
36
+ }
37
+ export function useMediator() {
38
+ const mediator = useContext(MediatorContext);
39
+ if (!mediator) {
40
+ throw new Error("useMediator needs a <StackProvider> above it.");
41
+ }
42
+ return mediator;
43
+ }
44
+ export function useStrings() {
45
+ const strings = useContext(StringsContext);
46
+ if (!strings) {
47
+ throw new Error("useStrings needs a <StackProvider> above it.");
48
+ }
49
+ return strings;
50
+ }
@@ -0,0 +1,52 @@
1
+ export type FieldKind = "string" | "guid" | "boolean" | "integer" | "number" | "date" | "datetime" | "time" | "duration" | "enum" | "array" | "map" | "object" | "unknown";
2
+ export type Binding = "route" | "query" | "header" | "body";
3
+ export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
4
+ export interface EnumType<TValue extends string = string> {
5
+ readonly key: string;
6
+ readonly values: readonly TValue[];
7
+ }
8
+ /** What the generator knows about one member: its wire kind, its localization key, how it
9
+ * binds and which of the message's DataAnnotations it carries. */
10
+ export interface Field {
11
+ readonly kind: FieldKind;
12
+ readonly key: string;
13
+ readonly nullable?: true;
14
+ readonly enum?: EnumType;
15
+ readonly element?: FieldKind;
16
+ readonly binding?: Binding;
17
+ readonly required?: true;
18
+ readonly maxLength?: number;
19
+ readonly minLength?: number;
20
+ readonly min?: number;
21
+ readonly max?: number;
22
+ readonly format?: "email" | "phone" | "url";
23
+ readonly pattern?: string;
24
+ /** Validation attributes only the server judges — a coded rule, or one the generator could
25
+ * not construct — named without their "Attribute" suffix. */
26
+ readonly serverRules?: readonly string[];
27
+ }
28
+ export type Fields<T> = {
29
+ readonly [K in keyof T]-?: Field;
30
+ };
31
+ export interface MessageType<TRequest, TResult> {
32
+ readonly name: string;
33
+ readonly key: string;
34
+ readonly area: string | null;
35
+ readonly method: Method;
36
+ readonly path: string;
37
+ readonly fields: Fields<TRequest>;
38
+ /** Never set: carries the result type so send() can infer it. */
39
+ readonly __result?: TResult;
40
+ }
41
+ export interface ModelType<T> {
42
+ readonly key: string;
43
+ readonly fields: Fields<T>;
44
+ }
45
+ export type RequestOf<M> = M extends MessageType<infer TRequest, unknown> ? TRequest : never;
46
+ export type ResultOf<M> = M extends MessageType<unknown, infer TResult> ? TResult : never;
47
+ export declare function defineEnum<TValue extends string>(key: string, values: readonly TValue[]): EnumType<TValue>;
48
+ export declare function defineModel<T>(key: string, fields: Fields<T>): ModelType<T>;
49
+ export declare function defineMessage<TRequest, TResult>(descriptor: Omit<MessageType<TRequest, TResult>, "__result">): MessageType<TRequest, TResult>;
50
+ /** The C# member a field was generated from — the last segment of its key, which is what a
51
+ * server Issue names in its source. */
52
+ export declare function memberOf(field: Field): string;
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ export function defineEnum(key, values) {
4
+ return { key, values };
5
+ }
6
+ export function defineModel(key, fields) {
7
+ return { key, fields };
8
+ }
9
+ export function defineMessage(descriptor) {
10
+ return descriptor;
11
+ }
12
+ /** The C# member a field was generated from — the last segment of its key, which is what a
13
+ * server Issue names in its source. */
14
+ export function memberOf(field) {
15
+ return field.key.slice(field.key.lastIndexOf(".") + 1);
16
+ }
package/dist/form.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { type MessageType } from "./descriptors";
2
+ import type { Issue, Problem } from "./problems";
3
+ export interface Form<T> {
4
+ readonly message: MessageType<T, unknown>;
5
+ readonly value: T;
6
+ readonly submitting: boolean;
7
+ /** The last send landed and nothing changed since. */
8
+ readonly saved: boolean;
9
+ /** What the server last refused, kept until the field it is about changes. */
10
+ readonly problem: Problem | null;
11
+ set<K extends keyof T>(name: K, value: T[K]): void;
12
+ reset(value: T): void;
13
+ issues(name: keyof T): Issue[];
14
+ submit(action: (value: T) => Promise<void>): Promise<boolean>;
15
+ }
16
+ /** The state of one message being filled in: its value, its refusals (the descriptor's rules
17
+ * checked as the user types, the server's answer after a send) and whether a send is running. */
18
+ export declare function useForm<T>(message: MessageType<T, unknown>, initial: T): Form<T>;
package/dist/form.js ADDED
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { memberOf } from "./descriptors";
5
+ import { validate, validateField } from "./validation";
6
+ import { problemOf } from "./load";
7
+ /** The state of one message being filled in: its value, its refusals (the descriptor's rules
8
+ * checked as the user types, the server's answer after a send) and whether a send is running. */
9
+ export function useForm(message, initial) {
10
+ const [value, setValue] = useState(initial);
11
+ const [touched, setTouched] = useState(new Set());
12
+ const [problem, setProblem] = useState(null);
13
+ const [submitting, setSubmitting] = useState(false);
14
+ const [saved, setSaved] = useState(false);
15
+ const set = useCallback((name, next) => {
16
+ setValue((current) => ({ ...current, [name]: next }));
17
+ setTouched((current) => new Set(current).add(name));
18
+ setSaved(false);
19
+ const member = memberOf(message.fields[name]);
20
+ setProblem((current) => (current ? { ...current, issues: current.issues.filter((issue) => issue.source !== member) } : current));
21
+ }, [message]);
22
+ const reset = useCallback((next) => {
23
+ setValue(next);
24
+ setTouched(new Set());
25
+ setProblem(null);
26
+ }, []);
27
+ const issues = useCallback((name) => {
28
+ const field = message.fields[name];
29
+ const member = memberOf(field);
30
+ const local = touched.has(name) ? validateField(field, value[name]) : [];
31
+ const remote = problem?.issues.filter((issue) => issue.source === member) ?? [];
32
+ return [...local, ...remote.filter((r) => !local.some((l) => l.code === r.code))];
33
+ }, [message, touched, value, problem]);
34
+ const submit = useCallback(async (action) => {
35
+ const refused = validate(message, value);
36
+ if (refused) {
37
+ setTouched(new Set(Object.keys(message.fields)));
38
+ setProblem(refused);
39
+ return false;
40
+ }
41
+ setSubmitting(true);
42
+ setSaved(false);
43
+ setProblem(null);
44
+ try {
45
+ await action(value);
46
+ setSaved(true);
47
+ return true;
48
+ }
49
+ catch (error) {
50
+ setProblem(problemOf(error));
51
+ return false;
52
+ }
53
+ finally {
54
+ setSubmitting(false);
55
+ }
56
+ }, [message, value]);
57
+ return useMemo(() => ({ message, value, submitting, saved, problem, set, reset, issues, submit }), [message, value, submitting, saved, problem, set, reset, issues, submit]);
58
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./descriptors";
2
+ export * from "./problems";
3
+ export * from "./validation";
4
+ export * from "./mediator";
5
+ export * from "./strings";
6
+ export * from "./context";
7
+ export * from "./load";
8
+ export * from "./form";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ export * from "./descriptors";
4
+ export * from "./problems";
5
+ export * from "./validation";
6
+ export * from "./mediator";
7
+ export * from "./strings";
8
+ export * from "./context";
9
+ export * from "./load";
10
+ export * from "./form";
package/dist/load.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { MessageType } from "./descriptors";
2
+ import { type Problem } from "./problems";
3
+ export type Load<T> = {
4
+ state: "loading";
5
+ data: T | undefined;
6
+ problem: null;
7
+ reload: () => void;
8
+ } | {
9
+ state: "loaded";
10
+ data: T;
11
+ problem: null;
12
+ reload: () => void;
13
+ } | {
14
+ state: "failed";
15
+ data: T | undefined;
16
+ problem: Problem;
17
+ reload: () => void;
18
+ };
19
+ export declare function problemOf(error: unknown): Problem;
20
+ /** Reads a query and keeps it current: the request is compared by value, so a new object with
21
+ * the same filters does not read twice, and an answer that lost the race is dropped. */
22
+ export declare function useLoad<TRequest, TResult>(message: MessageType<TRequest, TResult>, request: TRequest): Load<TResult>;
package/dist/load.js ADDED
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { BusinessError, requestFailed } from "./problems";
5
+ import { useMediator } from "./context";
6
+ export function problemOf(error) {
7
+ return error instanceof BusinessError ? error.problem : requestFailed(null, null);
8
+ }
9
+ /** Reads a query and keeps it current: the request is compared by value, so a new object with
10
+ * the same filters does not read twice, and an answer that lost the race is dropped. */
11
+ export function useLoad(message, request) {
12
+ const mediator = useMediator();
13
+ const identity = JSON.stringify(request);
14
+ const [version, setVersion] = useState(0);
15
+ const [load, setLoad] = useState({
16
+ state: "loading",
17
+ data: undefined,
18
+ problem: null,
19
+ });
20
+ const latest = useRef(request);
21
+ latest.current = request;
22
+ useEffect(() => {
23
+ const abort = new AbortController();
24
+ setLoad((current) => ({ state: "loading", data: current.data, problem: null }));
25
+ mediator
26
+ .send(message, latest.current, { signal: abort.signal })
27
+ .then((data) => setLoad({ state: "loaded", data, problem: null }))
28
+ .catch((error) => {
29
+ if (!abort.signal.aborted) {
30
+ setLoad((current) => ({ state: "failed", data: current.data, problem: problemOf(error) }));
31
+ }
32
+ });
33
+ return () => abort.abort();
34
+ }, [mediator, message, identity, version]);
35
+ const reload = useCallback(() => setVersion((v) => v + 1), []);
36
+ return { ...load, reload };
37
+ }
@@ -0,0 +1,23 @@
1
+ import type { MessageType } from "./descriptors";
2
+ export interface MediatorOptions {
3
+ /** Origin the API lives at; empty means the host that served the page. */
4
+ baseUrl?: string;
5
+ fetch?: typeof fetch;
6
+ /** Headers every send carries (a language, a tenant). Read per send. */
7
+ headers?: () => Record<string, string>;
8
+ }
9
+ export interface SendOptions {
10
+ signal?: AbortSignal;
11
+ }
12
+ /** Sends a message to the endpoint its [Http] declares: the client half of HttpSender. The
13
+ * request's own DataAnnotations are checked before anything leaves, as SendPipeline does. */
14
+ export declare class Mediator {
15
+ #private;
16
+ constructor(options?: MediatorOptions);
17
+ send<TRequest, TResult>(message: MessageType<TRequest, TResult>, request: TRequest, options?: SendOptions): Promise<TResult>;
18
+ url<TRequest>(message: MessageType<TRequest, unknown>, request: TRequest): string;
19
+ private hasBody;
20
+ private bodyOf;
21
+ private headersOf;
22
+ private problemOf;
23
+ }
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ import { BusinessError, isProblem, requestFailed } from "./problems";
4
+ import { validate } from "./validation";
5
+ function format(value) {
6
+ return typeof value === "string" ? value : String(value);
7
+ }
8
+ /** Sends a message to the endpoint its [Http] declares: the client half of HttpSender. The
9
+ * request's own DataAnnotations are checked before anything leaves, as SendPipeline does. */
10
+ export class Mediator {
11
+ #baseUrl;
12
+ #fetch;
13
+ #headers;
14
+ constructor(options = {}) {
15
+ this.#baseUrl = (options.baseUrl ?? "").replace(/\/+$/, "");
16
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
17
+ this.#headers = options.headers ?? (() => ({}));
18
+ }
19
+ async send(message, request, options = {}) {
20
+ const refused = validate(message, request);
21
+ if (refused) {
22
+ throw new BusinessError(refused);
23
+ }
24
+ let response;
25
+ try {
26
+ response = await this.#fetch(this.url(message, request), {
27
+ method: message.method,
28
+ headers: { Accept: "application/json", ...this.#headers(), ...this.headersOf(message, request), ...(this.hasBody(message) ? { "Content-Type": "application/json" } : {}) },
29
+ body: this.hasBody(message) ? JSON.stringify(this.bodyOf(message, request)) : undefined,
30
+ signal: options.signal,
31
+ });
32
+ }
33
+ catch (error) {
34
+ if (error instanceof DOMException && error.name === "AbortError") {
35
+ throw error;
36
+ }
37
+ throw new BusinessError(requestFailed(null, null));
38
+ }
39
+ const text = await response.text();
40
+ if (!response.ok) {
41
+ throw new BusinessError(this.problemOf(response.status, text));
42
+ }
43
+ if (text.length === 0) {
44
+ return undefined;
45
+ }
46
+ return JSON.parse(text);
47
+ }
48
+ url(message, request) {
49
+ const values = request;
50
+ const fields = Object.entries(message.fields);
51
+ const path = message.path.replace(/{([^{}:?]+)[^{}]*}/g, (_, token) => {
52
+ const name = fields.find(([candidate]) => candidate.toLowerCase() === token.toLowerCase())?.[0] ?? token;
53
+ return encodeURIComponent(format(values[name]));
54
+ });
55
+ const query = new URLSearchParams();
56
+ for (const [name, field] of fields) {
57
+ const value = values[name];
58
+ // An unset filter is an absent parameter, never "null" (HttpRequestMessageBuilder.AddQuery).
59
+ if (field.binding !== "query" || value === null || value === undefined) {
60
+ continue;
61
+ }
62
+ if (Array.isArray(value)) {
63
+ value.filter((item) => item !== null && item !== undefined).forEach((item) => query.append(name, format(item)));
64
+ }
65
+ else {
66
+ query.append(name, format(value));
67
+ }
68
+ }
69
+ const search = query.toString();
70
+ return `${this.#baseUrl}/${path}${search ? `?${search}` : ""}`;
71
+ }
72
+ hasBody(message) {
73
+ return message.method === "POST" || message.method === "PUT" || message.method === "PATCH";
74
+ }
75
+ bodyOf(message, request) {
76
+ const values = request;
77
+ const body = {};
78
+ for (const [name, field] of Object.entries(message.fields)) {
79
+ if (field.binding === "body" && values[name] !== undefined) {
80
+ body[name] = values[name];
81
+ }
82
+ }
83
+ return body;
84
+ }
85
+ headersOf(message, request) {
86
+ const values = request;
87
+ const headers = {};
88
+ for (const [name, field] of Object.entries(message.fields)) {
89
+ if (field.binding === "header" && values[name] !== null && values[name] !== undefined) {
90
+ headers[name] = format(values[name]);
91
+ }
92
+ }
93
+ return headers;
94
+ }
95
+ problemOf(status, text) {
96
+ if (text.length === 0) {
97
+ return requestFailed(status, "EmptyResponse");
98
+ }
99
+ try {
100
+ const parsed = JSON.parse(text);
101
+ return isProblem(parsed) ? { ...parsed, issues: parsed.issues ?? [] } : requestFailed(status, "StatusFallback");
102
+ }
103
+ catch {
104
+ return requestFailed(status, "DeserializationFailed");
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,23 @@
1
+ export interface Issue {
2
+ code: string;
3
+ message: string;
4
+ source?: string | null;
5
+ arguments?: Record<string, string> | null;
6
+ }
7
+ export interface Problem {
8
+ code: string;
9
+ title: string;
10
+ issues: Issue[];
11
+ status?: number | null;
12
+ }
13
+ /** The TypeScript face of BusinessException: an expected failure carrying the Problem the
14
+ * server (or the client's own validation) answered. */
15
+ export declare class BusinessError extends Error {
16
+ readonly problem: Problem;
17
+ constructor(problem: Problem);
18
+ }
19
+ export declare function isProblem(value: unknown): value is Problem;
20
+ /** The same codes NetworkProblem.RequestFailed carries, so one catalog translates both ends. */
21
+ export declare function requestFailed(status: number | null, reason: "EmptyResponse" | "DeserializationFailed" | "StatusFallback" | null): Problem;
22
+ /** Issues about one field, matched by the C# member name a server Issue names in its source. */
23
+ export declare function issuesFor(problem: Problem | null | undefined, member: string): Issue[];
@@ -0,0 +1,37 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ /** The TypeScript face of BusinessException: an expected failure carrying the Problem the
4
+ * server (or the client's own validation) answered. */
5
+ export class BusinessError extends Error {
6
+ problem;
7
+ constructor(problem) {
8
+ super(problem.title);
9
+ this.name = "BusinessError";
10
+ this.problem = problem;
11
+ }
12
+ }
13
+ export function isProblem(value) {
14
+ return (typeof value === "object" &&
15
+ value !== null &&
16
+ typeof value.code === "string" &&
17
+ typeof value.title === "string");
18
+ }
19
+ /** The same codes NetworkProblem.RequestFailed carries, so one catalog translates both ends. */
20
+ export function requestFailed(status, reason) {
21
+ const issues = [
22
+ status
23
+ ? { code: "HttpStatus", message: `The server responded with HTTP status code ${status}.` }
24
+ : { code: "NoResponse", message: "No response was received from the server." },
25
+ ];
26
+ if (reason) {
27
+ issues.push({ code: reason, message: reason });
28
+ }
29
+ return { code: "RequestFailed", title: "Unable to contact the server", issues, status };
30
+ }
31
+ /** Issues about one field, matched by the C# member name a server Issue names in its source. */
32
+ export function issuesFor(problem, member) {
33
+ if (!problem) {
34
+ return [];
35
+ }
36
+ return problem.issues.filter((issue) => issue.source === member);
37
+ }
@@ -0,0 +1,19 @@
1
+ import type { EnumType } from "./descriptors";
2
+ import type { Issue, Problem } from "./problems";
3
+ export declare function stackStrings(language: string): Record<string, string>;
4
+ /** StringManager, for the browser: a missing key renders as the key itself (or the fallback
5
+ * given), so an untranslated string is visible instead of guessed. */
6
+ export declare class Strings {
7
+ #private;
8
+ readonly language: string;
9
+ constructor(language: string, entries: Record<string, string>);
10
+ translate(key: string, fallback?: string): string;
11
+ /** Named ({name}) or positional ({0}) tokens filled from the arguments. */
12
+ format(key: string, args: Record<string, string> | readonly string[]): string;
13
+ count(key: string, count: number): string;
14
+ enumValue(type: EnumType, value: string | null | undefined): string;
15
+ /** "Problems.{Code}.{Source}", then "Problems.{Code}", then the message the sender wrote; the
16
+ * entity argument is a key and resolves to its label first. */
17
+ issue(issue: Issue): string;
18
+ problem(problem: Problem): string;
19
+ }
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ import mudBase from "./catalogs/strings.json";
4
+ import mudEs from "./catalogs/strings.es.json";
5
+ // The Stack's own sentences (Common.*, Actions.*, Problems.*) are the Blazor components'
6
+ // catalog, read from the very files NSail.Components.Mud embeds: one string in one file, whichever
7
+ // renderer draws it.
8
+ const stackCatalogs = { en: mudBase, es: { ...mudBase, ...mudEs } };
9
+ export function stackStrings(language) {
10
+ return stackCatalogs[language] ?? stackCatalogs.en;
11
+ }
12
+ const token = /{([A-Za-z0-9_]+)}/g;
13
+ function fill(text, args) {
14
+ if (!args) {
15
+ return text;
16
+ }
17
+ return text.replace(token, (whole, name) => (name in args ? args[name] : whole));
18
+ }
19
+ /** StringManager, for the browser: a missing key renders as the key itself (or the fallback
20
+ * given), so an untranslated string is visible instead of guessed. */
21
+ export class Strings {
22
+ language;
23
+ #entries;
24
+ constructor(language, entries) {
25
+ this.language = language;
26
+ this.#entries = entries;
27
+ }
28
+ translate(key, fallback) {
29
+ return this.#entries[key] ?? fallback ?? key;
30
+ }
31
+ /** Named ({name}) or positional ({0}) tokens filled from the arguments. */
32
+ format(key, args) {
33
+ const named = Array.isArray(args) ? Object.fromEntries(args.map((value, index) => [String(index), value])) : args;
34
+ return fill(this.translate(key), named);
35
+ }
36
+ count(key, count) {
37
+ const plural = count !== 1 ? this.#entries[`${key}.Plural`] : undefined;
38
+ return fill(plural ?? this.translate(key), { "0": String(count), count: String(count) });
39
+ }
40
+ enumValue(type, value) {
41
+ if (value === null || value === undefined) {
42
+ return "";
43
+ }
44
+ return this.translate(`${type.key}.${value}`, value);
45
+ }
46
+ /** "Problems.{Code}.{Source}", then "Problems.{Code}", then the message the sender wrote; the
47
+ * entity argument is a key and resolves to its label first. */
48
+ issue(issue) {
49
+ const scoped = issue.source ? this.#entries[`Problems.${issue.code}.${issue.source}`] : undefined;
50
+ const text = scoped ?? this.#entries[`Problems.${issue.code}`] ?? issue.message;
51
+ const args = issue.arguments ? { ...issue.arguments } : null;
52
+ if (args?.entity) {
53
+ args.entity = this.translate(args.entity, args.entity.slice(args.entity.lastIndexOf(".") + 1));
54
+ }
55
+ return fill(text, args);
56
+ }
57
+ problem(problem) {
58
+ return this.translate(`Problems.${problem.code}.Title`, problem.title);
59
+ }
60
+ }
@@ -0,0 +1,6 @@
1
+ import { type Field, type MessageType } from "./descriptors";
2
+ import type { Issue, Problem } from "./problems";
3
+ export declare function validateField(field: Field, value: unknown): Issue[];
4
+ /** The problem describing every rule the request breaks, or null — the client half of the
5
+ * check SendPipeline runs on both sides. */
6
+ export declare function validate<TRequest>(message: MessageType<TRequest, unknown>, request: TRequest): Problem | null;
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Leonardo Porro and Emmanuel Arias. https://github.com/nsail-ar/nsail-stack
3
+ import { memberOf } from "./descriptors";
4
+ // MessageValidator's vocabulary, code for code and argument for argument: the sentence under a
5
+ // field reads the same whether the browser refused the value or the server did.
6
+ const email = /^[^@\s]+@[^@\s]+$/;
7
+ const phone = /^\+?(?=.*\d)[\d\s().\-]+((x|ext\.?)\s?\d+)?$/i;
8
+ const url = /^(https?|ftp):\/\//i;
9
+ function issue(code, message, field, args = {}) {
10
+ return { code, message, source: field, arguments: { field, ...args } };
11
+ }
12
+ function isBlank(value) {
13
+ return value === null || value === undefined || (typeof value === "string" && value.trim().length === 0);
14
+ }
15
+ function length(value) {
16
+ if (typeof value === "string" || Array.isArray(value)) {
17
+ return value.length;
18
+ }
19
+ return null;
20
+ }
21
+ export function validateField(field, value) {
22
+ const name = memberOf(field);
23
+ const issues = [];
24
+ if (field.required && isBlank(value)) {
25
+ issues.push(issue("Required", `The field ${name} is required`, name));
26
+ }
27
+ // Every BCL attribute but Required passes a null: absence is Required's question alone.
28
+ if (value === null || value === undefined) {
29
+ return issues;
30
+ }
31
+ const size = length(value);
32
+ if (field.maxLength !== undefined && size !== null && size > field.maxLength) {
33
+ issues.push(issue("MaxLength", `The field ${name} exceeds the maximum length of ${field.maxLength}`, name, { max: String(field.maxLength) }));
34
+ }
35
+ if (field.minLength !== undefined && size !== null && size < field.minLength) {
36
+ issues.push(issue("Invalid", `The field ${name} must have at least ${field.minLength} characters`, name));
37
+ }
38
+ if ((field.min !== undefined || field.max !== undefined) && typeof value === "number") {
39
+ if ((field.min !== undefined && value < field.min) || (field.max !== undefined && value > field.max)) {
40
+ issues.push(issue("OutOfRange", `The field ${name} must be between ${field.min} and ${field.max}`, name, { from: String(field.min), to: String(field.max) }));
41
+ }
42
+ }
43
+ if (typeof value === "string" && value.length > 0) {
44
+ const formatted = (field.format === "email" && !email.test(value)) ||
45
+ (field.format === "phone" && !phone.test(value)) ||
46
+ (field.format === "url" && !url.test(value)) ||
47
+ (field.pattern !== undefined && !new RegExp(`^(?:${field.pattern})$`).test(value));
48
+ if (formatted) {
49
+ issues.push(issue("InvalidFormat", `The field ${name} has an invalid format`, name));
50
+ }
51
+ }
52
+ return issues;
53
+ }
54
+ /** The problem describing every rule the request breaks, or null — the client half of the
55
+ * check SendPipeline runs on both sides. */
56
+ export function validate(message, request) {
57
+ const issues = [];
58
+ const values = request;
59
+ for (const [name, field] of Object.entries(message.fields)) {
60
+ issues.push(...validateField(field, values[name]));
61
+ }
62
+ if (issues.length === 0) {
63
+ return null;
64
+ }
65
+ return { code: "InvalidModel", title: "One or more fields contain invalid data", issues, status: 400 };
66
+ }
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@nsail/stack",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.135",
4
+ "description": "NSail's vendor-free client half: sends messages from the descriptors an Sdk generates, speaks the catalog, and holds the hooks a page reads with.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/nsail-ar/nsail-stack.git",
9
+ "directory": "src/Stack/React/NSail.React.Stack"
10
+ },
11
+ "homepage": "https://github.com/nsail-ar/nsail-stack#readme",
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": {
16
+ "@nsail/source": "./src/index.ts",
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "node build.mjs",
29
+ "test": "vitest run",
30
+ "typecheck": "tsc --noEmit"
31
+ },
32
+ "peerDependencies": {
33
+ "react": ">=19"
34
+ }
35
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.