rastack 0.0.26 → 0.0.28

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,147 @@
1
+ import React from "react";
2
+ import { tokens, mergeStyles } from "../style";
3
+ import type { CellAlign } from "../util";
4
+
5
+ export type { CellAlign };
6
+
7
+ export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {}
8
+
9
+ /** A token-styled `<table>`. Compose with the head/body/row/cell primitives, or drive it with `<DataTable>`. */
10
+ export function Table({
11
+ style,
12
+ children,
13
+ ...rest
14
+ }: TableProps): React.JSX.Element {
15
+ const base: React.CSSProperties = {
16
+ width: "100%",
17
+ borderCollapse: "collapse",
18
+ fontFamily: tokens.font.sans,
19
+ fontSize: "0.875rem",
20
+ color: tokens.color.text,
21
+ textAlign: "left",
22
+ };
23
+ return (
24
+ <table style={mergeStyles(base, style)} {...rest}>
25
+ {children}
26
+ </table>
27
+ );
28
+ }
29
+
30
+ export function THead({
31
+ style,
32
+ children,
33
+ ...rest
34
+ }: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
35
+ return (
36
+ <thead style={style} {...rest}>
37
+ {children}
38
+ </thead>
39
+ );
40
+ }
41
+
42
+ export function TBody({
43
+ style,
44
+ children,
45
+ ...rest
46
+ }: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
47
+ return (
48
+ <tbody style={style} {...rest}>
49
+ {children}
50
+ </tbody>
51
+ );
52
+ }
53
+
54
+ export interface TrProps extends React.HTMLAttributes<HTMLTableRowElement> {}
55
+
56
+ export function Tr({ style, children, ...rest }: TrProps): React.JSX.Element {
57
+ return (
58
+ <tr style={style} {...rest}>
59
+ {children}
60
+ </tr>
61
+ );
62
+ }
63
+
64
+ export interface ThProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
65
+ align?: CellAlign;
66
+ sortable?: boolean;
67
+ /** The current sort on this column, for the indicator. */
68
+ sortDirection?: "asc" | "desc";
69
+ }
70
+
71
+ /** A header cell. When `sortable`, shows a direction indicator and a pointer cursor. */
72
+ export function Th({
73
+ align = "left",
74
+ sortable = false,
75
+ sortDirection,
76
+ style,
77
+ children,
78
+ ...rest
79
+ }: ThProps): React.JSX.Element {
80
+ const base: React.CSSProperties = {
81
+ padding: `${tokens.space.sm} ${tokens.space.md}`,
82
+ textAlign: align,
83
+ fontSize: "0.75rem",
84
+ fontWeight: tokens.font.weightMedium as unknown as number,
85
+ letterSpacing: "0.02em",
86
+ textTransform: "uppercase",
87
+ color: tokens.color.textMuted,
88
+ borderBottom: `1px solid ${tokens.color.border}`,
89
+ background: tokens.color.surfaceAlt,
90
+ whiteSpace: "nowrap",
91
+ cursor: sortable ? "pointer" : undefined,
92
+ userSelect: sortable ? "none" : undefined,
93
+ };
94
+ const indicator = sortable
95
+ ? sortDirection === "asc"
96
+ ? " ▲"
97
+ : sortDirection === "desc"
98
+ ? " ▼"
99
+ : " ↕"
100
+ : null;
101
+ return (
102
+ <th
103
+ style={mergeStyles(base, style)}
104
+ aria-sort={
105
+ sortDirection === "asc"
106
+ ? "ascending"
107
+ : sortDirection === "desc"
108
+ ? "descending"
109
+ : undefined
110
+ }
111
+ {...rest}
112
+ >
113
+ {children}
114
+ {indicator ? (
115
+ <span
116
+ style={{ opacity: sortDirection ? 0.9 : 0.35, fontSize: "0.7em" }}
117
+ >
118
+ {indicator}
119
+ </span>
120
+ ) : null}
121
+ </th>
122
+ );
123
+ }
124
+
125
+ export interface TdProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
126
+ align?: CellAlign;
127
+ }
128
+
129
+ export function Td({
130
+ align = "left",
131
+ style,
132
+ children,
133
+ ...rest
134
+ }: TdProps): React.JSX.Element {
135
+ const base: React.CSSProperties = {
136
+ padding: `${tokens.space.sm} ${tokens.space.md}`,
137
+ textAlign: align,
138
+ borderBottom: `1px solid ${tokens.color.border}`,
139
+ color: tokens.color.text,
140
+ verticalAlign: "middle",
141
+ };
142
+ return (
143
+ <td style={mergeStyles(base, style)} {...rest}>
144
+ {children}
145
+ </td>
146
+ );
147
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * `rastack/components` — the styling primitives.
3
+ *
4
+ * Every component in this package is painted **entirely from design tokens**:
5
+ * there is not a single hard-coded colour or dimension in the render tree, only
6
+ * `var(--…)` references produced here. That is what makes the kit "use the
7
+ * design system tokens as inputs" — drop the components inside a
8
+ * `<ThemeProvider>` (or link a `rastack tokens`-generated `tokens.css`) and they
9
+ * inherit the app's brand, spacing, radius and light/dark modes for free, with
10
+ * mode switches recascading through CSS with no re-render (exactly like
11
+ * `rastack/theme`).
12
+ *
13
+ * This module is pure (no React, no DOM), so the token-name mapping is unit
14
+ * tested the same way the token engine is.
15
+ */
16
+
17
+ import type React from "react";
18
+ import { cssVarName } from "rastack/tokens";
19
+
20
+ /**
21
+ * A design-token path (`"color.surface"`, `"space.md"`, `"radius.lg"`) rendered
22
+ * as a CSS custom-property reference. Reuses the token engine's `cssVarName`, so
23
+ * the variable names are guaranteed identical to what `rastack tokens` emits and
24
+ * `<ThemeProvider>` injects.
25
+ *
26
+ * An optional `fallback` becomes the second argument of `var()`, so a component
27
+ * still renders sensibly when a token set omits an optional token (e.g. a
28
+ * hand-authored theme without `color.danger`) or before any theme is wired up.
29
+ *
30
+ * ```ts
31
+ * cssVar("color.surface") // "var(--color-surface)"
32
+ * cssVar("color.danger", "#E5484D") // "var(--color-danger, #E5484D)"
33
+ * ```
34
+ */
35
+ export function cssVar(path: string, fallback?: string): string {
36
+ const name = cssVarName(path);
37
+ return fallback ? `var(${name}, ${fallback})` : `var(${name})`;
38
+ }
39
+
40
+ /**
41
+ * The semantic tokens the kit paints from, each already a `var(--…)` string.
42
+ * Colours carry a literal fallback that mirrors `defineTheme`'s defaults, so the
43
+ * components degrade gracefully with partial token sets and are legible even
44
+ * with no `<ThemeProvider>` at all — while a real theme still wins.
45
+ */
46
+ export const tokens = {
47
+ color: {
48
+ bg: cssVar("color.bg", "#ffffff"),
49
+ surface: cssVar("color.surface", "#ffffff"),
50
+ surfaceAlt: cssVar("color.surface-alt", "#f2f5fb"),
51
+ border: cssVar("color.border", "#e6eaf2"),
52
+ text: cssVar("color.text", "#0b1020"),
53
+ textMuted: cssVar("color.text-muted", "#4b5670"),
54
+ accent: cssVar("color.accent", "#4f6bff"),
55
+ accentStrong: cssVar("color.accent-strong", "#3b54e8"),
56
+ onAccent: cssVar("color.on-accent", "#ffffff"),
57
+ success: cssVar("color.success", "#1fa971"),
58
+ danger: cssVar("color.danger", "#e5484d"),
59
+ warning: cssVar("color.warning", "#f5a623"),
60
+ },
61
+ space: {
62
+ xs: cssVar("space.xs", "4px"),
63
+ sm: cssVar("space.sm", "8px"),
64
+ md: cssVar("space.md", "16px"),
65
+ lg: cssVar("space.lg", "24px"),
66
+ xl: cssVar("space.xl", "40px"),
67
+ },
68
+ radius: {
69
+ sm: cssVar("radius.sm", "6px"),
70
+ md: cssVar("radius.md", "10px"),
71
+ lg: cssVar("radius.lg", "16px"),
72
+ xl: cssVar("radius.xl", "24px"),
73
+ pill: cssVar("radius.pill", "999px"),
74
+ },
75
+ font: {
76
+ sans: cssVar("font.family.sans", "system-ui, sans-serif"),
77
+ mono: cssVar("font.family.mono", "ui-monospace, monospace"),
78
+ weightRegular: cssVar("font.weight.regular", "400"),
79
+ weightMedium: cssVar("font.weight.medium", "500"),
80
+ weightBold: cssVar("font.weight.bold", "700"),
81
+ },
82
+ shadow: {
83
+ sm: cssVar("shadow.sm", "0 1px 2px rgba(11,16,32,0.08)"),
84
+ md: cssVar(
85
+ "shadow.md",
86
+ "0 2px 4px rgba(11,16,32,0.12), 0 8px 24px rgba(11,16,32,0.10)",
87
+ ),
88
+ },
89
+ duration: {
90
+ fast: cssVar("duration.fast", "150ms"),
91
+ base: cssVar("duration.base", "200ms"),
92
+ },
93
+ } as const;
94
+
95
+ /** Shallow-merge a base style with a caller override (override wins, `undefined` ignored). */
96
+ export function mergeStyles(
97
+ base: React.CSSProperties,
98
+ override?: React.CSSProperties,
99
+ ): React.CSSProperties {
100
+ return override ? { ...base, ...override } : base;
101
+ }
102
+
103
+ // --- shared control tokens (kept in one place so inputs/selects/buttons align) --
104
+
105
+ /** Sizes shared by the interactive controls. */
106
+ export type ControlSize = "sm" | "md" | "lg";
107
+
108
+ /** Per-size padding / font-size / height, in token units. */
109
+ export const controlSizing: Record<
110
+ ControlSize,
111
+ { paddingY: string; paddingX: string; fontSize: string; height: string }
112
+ > = {
113
+ sm: {
114
+ paddingY: tokens.space.xs,
115
+ paddingX: tokens.space.sm,
116
+ fontSize: "0.8125rem",
117
+ height: "2rem",
118
+ },
119
+ md: {
120
+ paddingY: tokens.space.sm,
121
+ paddingX: tokens.space.md,
122
+ fontSize: "0.875rem",
123
+ height: "2.5rem",
124
+ },
125
+ lg: {
126
+ paddingY: tokens.space.sm,
127
+ paddingX: tokens.space.lg,
128
+ fontSize: "1rem",
129
+ height: "3rem",
130
+ },
131
+ };
132
+
133
+ /** The base look every text-like control shares (input, textarea, select). */
134
+ export function controlBaseStyle(size: ControlSize): React.CSSProperties {
135
+ const s = controlSizing[size];
136
+ return {
137
+ display: "block",
138
+ width: "100%",
139
+ boxSizing: "border-box",
140
+ padding: `${s.paddingY} ${s.paddingX}`,
141
+ fontFamily: tokens.font.sans,
142
+ fontSize: s.fontSize,
143
+ color: tokens.color.text,
144
+ background: tokens.color.surface,
145
+ border: `1px solid ${tokens.color.border}`,
146
+ borderRadius: tokens.radius.md,
147
+ outline: "none",
148
+ transition: `border-color ${tokens.duration.fast} ease, box-shadow ${tokens.duration.fast} ease`,
149
+ };
150
+ }
151
+
152
+ /** The focus ring shared by controls, as inline style to spread on focus. */
153
+ export const focusRingStyle: React.CSSProperties = {
154
+ borderColor: tokens.color.accent,
155
+ boxShadow: `0 0 0 3px ${cssVar("color.accent", "#4f6bff")}33`,
156
+ };
157
+
158
+ /** Style applied to a control flagged invalid. */
159
+ export const invalidControlStyle: React.CSSProperties = {
160
+ borderColor: tokens.color.danger,
161
+ };
162
+
163
+ // --- button variants ----------------------------------------------------------
164
+
165
+ export type ButtonVariant =
166
+ "primary" | "secondary" | "outline" | "ghost" | "danger";
167
+
168
+ /** Colour treatment per button variant (structure/sizing handled by the component). */
169
+ export const buttonVariantStyle: Record<ButtonVariant, React.CSSProperties> = {
170
+ primary: {
171
+ background: tokens.color.accent,
172
+ color: tokens.color.onAccent,
173
+ border: "1px solid transparent",
174
+ },
175
+ secondary: {
176
+ background: tokens.color.surfaceAlt,
177
+ color: tokens.color.text,
178
+ border: `1px solid ${tokens.color.border}`,
179
+ },
180
+ outline: {
181
+ background: "transparent",
182
+ color: tokens.color.text,
183
+ border: `1px solid ${tokens.color.border}`,
184
+ },
185
+ ghost: {
186
+ background: "transparent",
187
+ color: tokens.color.text,
188
+ border: "1px solid transparent",
189
+ },
190
+ danger: {
191
+ background: tokens.color.danger,
192
+ color: "#ffffff",
193
+ border: "1px solid transparent",
194
+ },
195
+ };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Structural contracts the data-connected components accept.
3
+ *
4
+ * The framework has **two** hook families (see `docs/local-sync-engine.md`):
5
+ * the REST list hook returns a rich `IListQuery` (`data` + `header` +
6
+ * `pagination`/`sorting`/`searching`/`filtering` + the react-query flags), while
7
+ * a `sync: { mode: "local" }` entity returns only `{ data, isLoading, isReady }`.
8
+ * Rather than couple to either, the components accept the **structural
9
+ * lowest-common-denominator** below and *feature-detect* the richer parts — a
10
+ * `<DataTable>` renders a pager only when `pagination` is present, sortable
11
+ * headers only when `sorting` is present, and so on. Both hook families satisfy
12
+ * `TableQuery`, so the same `<DataTable query={…} />` works with either.
13
+ *
14
+ * These are `import type`-only re-exports of the real hook interfaces, so this
15
+ * file adds no runtime dependency and the shapes never drift from the hooks.
16
+ */
17
+
18
+ import type {
19
+ IPaginationActions,
20
+ ISearchingActions,
21
+ ISortingActions,
22
+ IFilterActions,
23
+ ReturnedHeader,
24
+ } from "../hooks/query/interfaces";
25
+ import type { FormFieldStructure } from "../hooks/form/interfaces";
26
+
27
+ export type { ReturnedHeader } from "../hooks/query/interfaces";
28
+
29
+ /**
30
+ * Anything a `<DataTable>` / `useDataTable` can drive. The only required members
31
+ * are `data` (the rows) and, in practice, a loading flag; everything else is
32
+ * optional and lit up when present.
33
+ */
34
+ export interface TableQuery<TRow = Record<string, unknown>> {
35
+ /** The rows. `IListQuery.data` (`.results`) or a sync list's `data`. */
36
+ data: TRow[] | undefined;
37
+ /** react-query's flag (REST) — absent on sync hooks, treated as `false`. */
38
+ isLoading?: boolean;
39
+ /** react-query's flag (REST). */
40
+ isError?: boolean;
41
+ /** react-query's error (REST). */
42
+ error?: unknown;
43
+ /** Column descriptors from the server envelope, when the API supplies them. */
44
+ header?: ReturnedHeader[];
45
+ /** Present on REST list hooks — enables the pager. */
46
+ pagination?: IPaginationActions;
47
+ /** Present on REST list hooks — enables the search box. */
48
+ searching?: ISearchingActions<Record<string, unknown>>;
49
+ /** Present on REST list hooks — enables sortable headers. */
50
+ sorting?: ISortingActions<Record<string, unknown>>;
51
+ /** Present on REST list hooks. */
52
+ filtering?: IFilterActions<Record<string, unknown>>;
53
+ /** Sync hooks expose readiness instead of react-query's `isLoading`. */
54
+ isReady?: boolean;
55
+ }
56
+
57
+ /**
58
+ * A single field as `useForm().getFormField(name)` returns it, with `value`
59
+ * widened to `unknown` and `setValue` to `(value: any)`. `FormFieldStructure<T>`
60
+ * is invariant in `T` (it both reads `value: T` and writes `setValue(v: T)`), so
61
+ * these two overrides are what let a `FormFieldStructure<SomeField>` assign here
62
+ * regardless of the field's concrete type.
63
+ */
64
+ export type AutoFormField = Omit<
65
+ FormFieldStructure<unknown>,
66
+ "value" | "setValue"
67
+ > & {
68
+ value: unknown;
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ setValue: (value: any) => void;
71
+ };
72
+
73
+ /**
74
+ * Anything an `<AutoForm>` / `useAutoForm` can drive — the subset of
75
+ * `useForm()`'s `FormStructure` the auto-form reads. `getFormField`/`onSubmit`/
76
+ * `resetForm` are declared as **methods** (bivariant params) so the generic
77
+ * `FormStructure` assigns without a cast.
78
+ */
79
+ export interface AutoFormLike {
80
+ /**
81
+ * Current values; their keys are the default field list/order. Typed as
82
+ * `object` (not `Record<string, unknown>`) so a `useForm<Interface>()` result —
83
+ * whose values are a plain interface with no index signature — assigns cleanly.
84
+ */
85
+ formValues: object | undefined;
86
+ /** Per-field structure, including the (enriched) datatype used to pick a control. */
87
+ getFormField(name: string): AutoFormField;
88
+ /** Validate + fire the wired mutation. */
89
+ onSubmit(): void;
90
+ /** Reset back to the initial values. */
91
+ resetForm(): void;
92
+ /** Whether submission is currently blocked (invalid). */
93
+ isSubmitDisabled: boolean;
94
+ /** Whether a submit mutation is in flight. */
95
+ isSubmitting: boolean;
96
+ /** Formik's error bag, keyed by field name. */
97
+ errors?: Record<string, unknown> | unknown;
98
+ }
@@ -0,0 +1,14 @@
1
+ /** Small pure helpers + shared types across the components. React- and DOM-free. */
2
+
3
+ /** Text alignment shared by table cells and (potentially) other layout. */
4
+ export type CellAlign = "left" | "center" | "right";
5
+
6
+ /** `"first_name"` / `"firstName"` / `"created-at"` → `"First name"`. */
7
+ export function humanizeKey(key: string): string {
8
+ const spaced = key
9
+ .replace(/[_\-.]+/g, " ")
10
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
11
+ .trim();
12
+ if (!spaced) return key;
13
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
14
+ }
@@ -160,10 +160,11 @@ export function useForm<
160
160
  const { setValue: setFormValue, setTouched } =
161
161
  formik.getFieldHelpers(fieldName);
162
162
 
163
- let { type, isRequired } = getFieldStructure(
164
- mutations ? mutations[updateMethod]?.validation : undefined,
165
- fieldName,
166
- );
163
+ const { type, isRequired, maxLength, options, relation, nullable, format } =
164
+ getFieldStructure(
165
+ mutations ? mutations[updateMethod]?.validation : undefined,
166
+ fieldName,
167
+ );
167
168
 
168
169
  const error = errorMessage ?? "";
169
170
  const isError = errorMessage !== undefined;
@@ -189,6 +190,11 @@ export function useForm<
189
190
  isRequired,
190
191
  setValue,
191
192
  setTouched,
193
+ maxLength,
194
+ options,
195
+ relation,
196
+ nullable,
197
+ format,
192
198
  };
193
199
  };
194
200
 
@@ -3,6 +3,18 @@ import { SchemaObject } from "ajv";
3
3
  import { IUpdateQueryResult } from "../../hooks";
4
4
  import { FormikValues } from "formik";
5
5
 
6
+ /** A choice for an `enum`-backed (`select`) field. */
7
+ export interface FieldOption {
8
+ label: string;
9
+ value: string | number;
10
+ }
11
+
12
+ /** The `{ app, model }` a foreign-key field points at (`x-rastack-relation`). */
13
+ export interface RelationTarget {
14
+ app: string;
15
+ model: string;
16
+ }
17
+
6
18
  export interface FormFieldStructure<TValue> {
7
19
  value: TValue;
8
20
  error: string;
@@ -13,6 +25,16 @@ export interface FormFieldStructure<TValue> {
13
25
  isRequired: boolean;
14
26
  setValue: (value: TValue) => void;
15
27
  setTouched: (isTouched: boolean) => void;
28
+ /** Max string length, when the schema constrains it. Drives a character counter. */
29
+ maxLength?: number;
30
+ /** Choices for a `select` field, derived from the schema's `enum`. */
31
+ options?: FieldOption[];
32
+ /** The FK target, when this field is a foreign key. Lets a caller render a relation picker. */
33
+ relation?: RelationTarget;
34
+ /** Whether the schema allows `null`. */
35
+ nullable?: boolean;
36
+ /** The schema `format` (`date-time`, `email`, `uuid`, …), for finer control choices. */
37
+ format?: string;
16
38
  }
17
39
 
18
40
  interface FormMutation<
@@ -118,7 +140,24 @@ export interface FormStructure<TFormValues, TResponse> {
118
140
  ) => FormFieldStructure<FlattenedRecord<TFormValues>[TFieldName]>;
119
141
  }
120
142
 
121
- export type FormType = "number" | "text" | "array";
143
+ /**
144
+ * The control kind a field maps to. Derived from the write JSON Schema by
145
+ * {@link getFieldStructure}: the base scalar types (`text`/`number`/`array`)
146
+ * plus the richer distinctions a form UI needs — `boolean` (checkbox), `date`
147
+ * / `datetime` (date pickers), `select` (an `enum`), and the typed-text formats
148
+ * `email` / `url`. The list is a superset of the original three, so existing
149
+ * `type === "number"` / `"text"` / `"array"` checks keep working.
150
+ */
151
+ export type FormType =
152
+ | "text"
153
+ | "number"
154
+ | "array"
155
+ | "boolean"
156
+ | "date"
157
+ | "datetime"
158
+ | "select"
159
+ | "email"
160
+ | "url";
122
161
 
123
162
  export interface SetupUpdateEndpointReturn<TParams, TPayload, TResult> {
124
163
  mutation: IUpdateQueryResult<TParams, TPayload, TResult>;
@@ -1,39 +1,96 @@
1
- import { SchemaObject } from "ajv";
2
- import { FormType } from "./interfaces";
1
+ import type { SchemaObject } from "ajv";
2
+ import type { FieldOption, FormType, RelationTarget } from "./interfaces";
3
3
 
4
- interface GetFieldStructureResponse {
4
+ export interface GetFieldStructureResponse {
5
5
  isRequired: boolean;
6
6
  type: FormType;
7
+ maxLength?: number;
8
+ options?: FieldOption[];
9
+ relation?: RelationTarget;
10
+ nullable?: boolean;
11
+ format?: string;
7
12
  }
8
13
 
14
+ /**
15
+ * Read one field's UI shape out of a write JSON Schema (the dereferenced
16
+ * `Deref{Name}` the form hook validates against).
17
+ *
18
+ * The schema is the only runtime artifact that still carries a field's real
19
+ * datatype — `type`, `format` (`date-time`/`email`/`uuid`), `enum`, `maxLength`,
20
+ * `nullable`, and the `x-rastack-relation` FK target the compiler stamps on
21
+ * foreign keys. This maps that to a {@link FormType} control kind plus the extra
22
+ * metadata a form UI needs, so `<AutoForm>` (and any hand-rolled form reading
23
+ * `getFormField`) can render the correct input per datatype instead of a text
24
+ * box for everything.
25
+ */
9
26
  export function getFieldStructure<TFieldName>(
10
27
  validationSchema: SchemaObject | undefined,
11
28
  fieldName: TFieldName,
12
29
  ): GetFieldStructureResponse {
13
- let type: FormType = "text";
14
- if (validationSchema?.properties) {
15
- const schemaJson = validationSchema.properties[fieldName];
16
- if (!schemaJson) {
17
- return { isRequired: false, type };
18
- }
19
- let schemaJsonType = schemaJson.type;
20
-
21
- if (typeof schemaJsonType === "string") {
22
- schemaJsonType = [schemaJsonType];
23
- }
24
- if (schemaJsonType) {
25
- if (schemaJsonType.includes("integer")) {
26
- type = "number";
27
- }
28
- if (schemaJsonType.includes("array")) {
29
- type = "array";
30
- }
31
- }
30
+ const result: GetFieldStructureResponse = { isRequired: false, type: "text" };
31
+
32
+ // JSON Schema shapes vary across ajv typings; access untyped.
33
+ const schema = validationSchema as
34
+ { properties?: Record<string, any>; required?: unknown } | undefined;
35
+ const property: any = schema?.properties?.[fieldName as string];
36
+ if (!property) return result;
37
+
38
+ // JSON Schema `type` can be a string or an array (nullable unions like
39
+ // `["string", "null"]`); normalise to a list.
40
+ const rawType = property.type;
41
+ const typeList: string[] =
42
+ typeof rawType === "string"
43
+ ? [rawType]
44
+ : Array.isArray(rawType)
45
+ ? rawType
46
+ : [];
47
+ if (typeList.includes("null") || property.nullable === true) {
48
+ result.nullable = true;
49
+ }
50
+
51
+ const format: unknown = property.format;
52
+ if (typeof format === "string") result.format = format;
53
+
54
+ if (typeof property.maxLength === "number")
55
+ result.maxLength = property.maxLength;
56
+
57
+ const relation = property["x-rastack-relation"];
58
+ if (relation && typeof relation === "object") {
59
+ result.relation = relation as RelationTarget;
32
60
  }
33
61
 
34
- let isRequired = false;
35
- if (validationSchema?.required) {
36
- isRequired = validationSchema.required?.includes(fieldName);
62
+ const enumValues: unknown = property.enum;
63
+
64
+ // Choose the control kind. Enum wins over the raw scalar type (a constrained
65
+ // string is a picker, not a free text box); FK metadata is advisory and does
66
+ // not change the underlying scalar control.
67
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
68
+ result.type = "select";
69
+ result.options = enumValues
70
+ .filter((v): v is string | number => v !== null && v !== undefined)
71
+ .map((v) => ({ label: String(v), value: v }));
72
+ } else if (typeList.includes("boolean")) {
73
+ result.type = "boolean";
74
+ } else if (typeList.includes("integer") || typeList.includes("number")) {
75
+ result.type = "number";
76
+ } else if (typeList.includes("array")) {
77
+ result.type = "array";
78
+ } else if (result.format === "date-time") {
79
+ result.type = "datetime";
80
+ } else if (result.format === "date") {
81
+ result.type = "date";
82
+ } else if (result.format === "email") {
83
+ result.type = "email";
84
+ } else if (result.format === "uri" || result.format === "url") {
85
+ result.type = "url";
86
+ } else {
87
+ result.type = "text";
37
88
  }
38
- return { isRequired, type };
89
+
90
+ const required: unknown = schema?.required;
91
+ if (Array.isArray(required)) {
92
+ result.isRequired = required.includes(fieldName);
93
+ }
94
+
95
+ return result;
39
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",