import { GraphQLField } from 'graphql'; import { GraphQLObjectType } from 'graphql'; import type { IntrospectionQuery } from 'graphql'; import { z } from 'zod'; /** * Action definition within a workflow * @public */ export declare const ActionDefinition: z.ZodObject<{ label: z.ZodString; requiredFields: z.ZodOptional>; allowedStates: z.ZodOptional>; nextState: z.ZodOptional; stateless: z.ZodOptional; selfTransition: z.ZodOptional; clientHandler: z.ZodOptional; }, z.core.$strip>; /** * Action definition type inferred from Zod schema * @public */ export declare type ActionDefinition = z.infer; /** * The name an entity's aggregate doctype is generated under: the entity's name, pluralised. * * One definition, because the CLI writes the file under `toSlug` of this and any later caller * (a scaffolder, a docs generator) must land on the same name or it silently addresses a * different file. * * `pluralize` rather than appending `s`, because the irregulars are not rare in practice — * measured against a consumer's 41 hand-authored aggregate doctypes, this rule reproduces every * one of their names, slugs and filenames exactly, while `+ 's'` gets five wrong * (`Currencys`, `JournalEntrys`, …). * * The rule is not total: an already-plural name pluralises to itself. Callers must handle that — * see {@link buildAggregateDoctype}. * * @param doctypeName - the entity doctype's `name` * @returns the aggregate doctype's `name` * @public * * @example * ```typescript * aggregateDoctypeName('SalesOrder') // 'SalesOrders' -> slug 'sales-orders' * ``` */ export declare function aggregateDoctypeName(doctypeName: string): string; /** * Reading an authored doctype — the JSON as it sits on disk, before any parsing. * * A separate reader from `@stonecrop/schema`'s `flattenFields`/`getPrimaryKeyField` because the two * operate on different *shapes*, not different rules: those take parsed `DoctypeField`s and branch * on the `kind` discriminant the Zod parser synthesizes, which authored JSON does not carry. * `getPrimaryKeyField` on a raw file therefore returns `undefined` — indistinguishable from "no key * declared", which is the exact condition its callers are testing. * * Every question about authored JSON is answered here once, so the rule cannot drift between the * merge and the generation plan. * * @internal */ /** * A doctype as it exists on disk: a plain object that may carry keys this package does not model * (`handler` on an action, `filterFunction` on a field, whatever an app has added). Typing it * loosely is what lets the merge round-trip those keys untouched instead of dropping them. * * @public */ export declare type AuthoredDoctype = Record; /** * Resolved badge for rendering. Returned by `format` or built from an options map. * @public */ export declare interface BadgeDescriptor { label: string; variant?: BadgeVariant; color?: string; } /** * Where ABadge paints the same descriptor. * @public */ export declare type BadgePresentation = 'cell-fill' | 'input-accent'; /** * Value in a choice→badge map: shorthand variant or object form. * @public */ export declare type BadgeSpec = BadgeVariant | BadgeSpecObject; /** * Per-choice badge configuration in a Select options map. * @public */ export declare interface BadgeSpecObject { variant?: BadgeVariant; color?: string; label?: string; } /** * Semantic badge variant. Maps to theme tokens `--sc-badge-{variant}-*`. * @public */ export declare type BadgeVariant = 'neutral' | 'success' | 'warning' | 'danger' | 'brand'; /** * Derive the aggregate doctype for a converted entity. * * Returns `undefined` when no identity column can be found — a natural-key table whose key the * converter refuses to guess and whose author has not declared one, or a foreign PostGraphile * endpoint that has left the Relay identifier occupying `id` (Stonecrop's own preset moves it to * `nodeId`). That is deliberate: an aggregate with an empty `fields` array is a valid doctype that * renders a table with no columns, which looks like a data problem rather than a generation one. * Emitting nothing and saying so is the loud failure. * * Identity resolves the same way `getRecordIdField` resolves it — the declared `primaryKey`, then * the conventional `id` — so an aggregate is always keyed on the column the client will later ask * for. `declaredIdentity` overrides both: SDL cannot express which `UNIQUE` column is the key, so * for a natural-key table the answer only exists in the authored file, and the caller that read it * passes the fieldname back. * * @param doctype - a converted entity doctype, as returned by `convertGraphQLSchema` * @param declaredIdentity - fieldname the authored doctype declares as its `primaryKey`, when the * caller has read one. Must name a field the converter emitted; the caller checks that, because * only it can say whether a missing one is a dropped column or a typo. * @returns the aggregate doctype, or `undefined` when no identity column exists * @public * * @example * ```typescript * const [order] = convertGraphQLSchema(sdl, { include: ['Order'] }) * const aggregate = buildAggregateDoctype(order) * // { name: 'Orders', slug: 'orders', fields: [ the id field ] } * ``` */ export declare function buildAggregateDoctype(doctype: ConvertedGraphQLDoctype, declaredIdentity?: string): ConvertedGraphQLDoctype | undefined; /** * Build a merged scalar map from the built-in maps and user-provided custom scalars. * Precedence (highest to lowest): customScalars → GQL_SCALAR_MAP → WELL_KNOWN_SCALARS * * @param customScalars - User-provided scalar overrides * @returns Merged scalar map * @public */ export declare function buildScalarMap(customScalars?: Record>): Record; /** * Converts camelCase to Title Case label * @param camelCase - Camel case string * @returns Title case label * @public * @example * ```typescript * camelToLabel('userEmail') // 'User Email' * camelToLabel('firstName') // 'First Name' * ``` */ export declare function camelToLabel(camelCase: string): string; /** * Converts camelCase to snake_case * @param camelCase - Camel case string * @returns Snake case string * @public * @example * ```typescript * camelToSnake('userEmail') // 'user_email' * camelToSnake('createdAt') // 'created_at' * ``` */ export declare function camelToSnake(camelCase: string): string; /** * Every component Stonecrop ships with that can render a value field, sorted by name. * * The union of the two maps above is the definition, not a copy of it: a shipped component either * categorises a value ({@link COMPONENT_CATEGORY}) or is one of the link containers that has no * value of its own ({@link COMPONENT_LINK_EXPANSION}'s `AForm`/`ATable`). `AFieldset` is absent by * the same rule — it is a `kind: 'fieldset'` container, so it is never a value field's component. * * `component` is an **open** axis: any string is valid, and naming a custom component is how an app * renders a field Stonecrop ships no widget for. This list is therefore the set to *suggest* to an * author, and to check first-party data against — never a set to validate arbitrary input against. * * @public */ export declare const CANONICAL_COMPONENTS: readonly string[]; /** * Cardinality for relationship links. * @public */ export declare const Cardinality: z.ZodEnum<{ atMostOne: "atMostOne"; one: "one"; noneOrMany: "noneOrMany"; atLeastOne: "atLeastOne"; }>; /** * Cardinality type inferred from Zod schema * @public */ export declare type Cardinality = z.infer; /** * Classify a single GraphQL field into a Stonecrop field definition. * * Classification rules (in order): * 1. Scalar types → look up in merged scalar map * 2. Enum types → `Select` with enum values as options * 3. Object types that are entities → `Link` with slug as options * 4. Object types that are Connections → `Doctype` with node type slug as options * 5. List of entity type → `Doctype` with item type slug as options * 6. Anything else → `Data` with `_unmapped: true` * * @param fieldName - The GraphQL field name * @param field - The GraphQL field definition * @param entityTypes - Set of type names classified as entities * @param options - Conversion options (for custom scalars, unmapped meta, etc.) * @returns The Stonecrop field definition * @public */ export declare function classifyFieldType(fieldName: string, field: GraphQLField, entityTypes: Set, options?: GraphQLConversionOptions): GraphQLConversionFieldMeta; /** * Authoring contract for doctype field declarations that can be rendered as table columns. * Pass a `ColumnSchema[]` array to ATable's `:schema` prop; `schemaToColumns` converts it * to `TableColumn[]` internally — callers working from a doctype schema never need to * construct `TableColumn` directly. * * Notes on specific properties: * - `align` uses an explicit string union rather than `CanvasTextAlign` — this package is * used server-side by the CLI where browser DOM types are absent. The values are identical. * - `format` is a serialized function string; the table store's `getFormattedValue` deserializes * it via `Function(...)`. `TableColumn.format` widens this to also accept a live function. * - `mask` is absent — it is function-typed only and cannot be serialized to JSON. It lives * exclusively on `TableColumn`. * - `modalComponent` is string-only — functions cannot appear in schema JSON. `TableColumn` * widens this to also accept a factory function. * * @public */ export declare interface ColumnSchema { /** Unique identifier for the field within its doctype. Maps to `name` on `TableColumn`. */ fieldname: string; /** * Rendering component (e.g. `'ATextInput'`, `'ANumericInput'`, `'ADate'`). Default cell * formatting and filter widgets derive from its {@link ComponentCategory}. * * Optional here, unlike `ValueField.component`: absence is what marks an entry as non-scalar * (a nested table or fieldset), which `schemaToColumns` excludes — it has no column equivalent. */ component?: string; /** * Target doctype slug — marks this column as a link. When set and no `cellComponent` is given, * `schemaToColumns` copies it to `TableColumn.linkDoctype`, which ACell uses to resolve a bare * id to display text. */ doctype?: string; /** * Human-readable column header. When absent, ATable assigns labels alphabetically * (A, B, C, …). */ label?: string; /** When `true`, the field is excluded from the derived columns by `schemaToColumns`. */ hidden?: boolean; /** * Horizontal text alignment for the column cell and header. * * @defaultValue 'center' */ align?: 'left' | 'right' | 'center' | 'start' | 'end'; /** * Whether the column cell is editable in the table. * * @defaultValue false */ edit?: boolean; /** * CSS width of the column (e.g. `'20ch'`, `'200px'`). * * @defaultValue '40ch' */ width?: string; /** * When `true`, the column is pinned to the left side of the table. * * @defaultValue false */ pinned?: boolean; /** * When `true`, the column can be resized by dragging the header edge. * * @defaultValue false */ resizable?: boolean; /** * When `true`, clicking the column header sorts the table by this column. * * @defaultValue true */ sortable?: boolean; /** * When `true`, a filter control is rendered in the column header. * * @defaultValue true */ filterable?: boolean; /** * The type of filter control to render. When absent, a default is derived from the * `component`'s {@link ComponentCategory} (`boolean` → `checkbox`, `date` → `date`, * `datetime` → `dateRange`, `select` → `select`, `number` → `number`, everything else * — including an unknown component — → `text`). */ filterType?: 'text' | 'select' | 'number' | 'date' | 'dateRange' | 'checkbox' | 'component'; /** * Static option list for `filterType: 'select'`. When absent, options are derived from * the unique values present in the column's rows. */ filterOptions?: any[]; /** Registered component name used when `filterType` is `'component'`. */ filterComponent?: string; /** * Registered component name rendered inside the table cell instead of the default display. * When absent, the table renders the value as plain text in a ``. */ cellComponent?: string; /** * Additional props passed to `cellComponent`. * * Only applicable when `cellComponent` is set. */ cellComponentProps?: Record; /** * Registered component name rendered in the cell's modal editor. String-only — functions * cannot appear in schema JSON. `TableColumn.modalComponent` widens this to also accept a * factory function. * * The following props are automatically passed to the modal component: * - `colIndex` — the column index of the current cell * - `rowIndex` — the row index of the current cell * - `store` — the table data store */ modalComponent?: string; /** * Extra props passed to `modalComponent` in addition to the standard cell props. * * Only applicable when `modalComponent` is set. */ modalComponentExtraProps?: Record; /** * Type-specific field options — Select choices, badge maps, quantity/currency config, etc. * Spreads through `schemaToColumns` to `TableColumn`. */ options?: FieldOptions; /** * Serialized function string used to format the cell value for display. Deserialized at * render time by the table store's `getFormattedValue`. May return a plain string, HTML, or a * {@link BadgeDescriptor}. `TableColumn.format` widens this to also accept a live function. */ format?: string; /** * When `true`, this column is treated as a Gantt bar column. * * Only applicable for Gantt tables. * * @defaultValue false */ isGantt?: boolean; /** * Registered component name used to render Gantt bars in this column. * * Only applicable for Gantt tables. * * @defaultValue 'AGanttCell' */ ganttComponent?: string; /** * Number of columns this Gantt bar spans across. When absent, the bar stretches to cover * all non-pinned columns in the table. * * Only applicable for Gantt tables. */ colspan?: number; } /** * Canonical component → semantic category. Only the components Stonecrop ships with appear here; * custom/unknown component names have no category and consumers fall back to their default. * @public */ export declare const COMPONENT_CATEGORY: Record; /** * Canonical link component → expansion. Only components Stonecrop ships with appear here; an * unmapped (custom) component has none, and callers treat that as `expand` — the behaviour that * predates this map, so a custom component can never silently collapse a link to a picker. * @public */ export declare const COMPONENT_LINK_EXPANSION: Record; /** * Semantic category for a rendering component. * * `component` is the primary field axis, so the runtime consumers that need to know what a field * *means* (atable cell formatting / filter widgets, record-default init) derive it from here. This * is the single source of "what kind of value does this component render", keyed by the canonical * registered component names — each consumer maps the category to its own concern (filter widget, * default value, …). * * @public */ export declare type ComponentCategory = 'text' | 'number' | 'boolean' | 'date' | 'datetime' | 'select' | 'code' | 'link' | 'attach' | 'quantity' | 'currency'; /** * Resolve a component's semantic category, or `undefined` for an unknown (custom) component — * callers treat that as "no opinion" and use their own default. * @public */ export declare function componentCategory(component?: string): ComponentCategory | undefined; /** * Resolve a component's link expansion, or `undefined` for an absent/unmapped component. * @public */ export declare function componentLinkExpansion(component?: string): LinkExpansion | undefined; /** * Output of GraphQL schema conversion — one per entity type. * * @public */ export declare interface ConvertedGraphQLDoctype extends Omit { /** Field definitions — GraphQL conversion metadata stripped; same shape as DoctypeMeta.fields */ fields: ValueField[]; /** Original GraphQL type name (for debugging/reference) */ _graphqlTypeName?: string; } /** * Convert a GraphQL schema to Stonecrop doctype schemas. * * Accepts either an `IntrospectionQuery` result object or an SDL string. * Entity types are identified using heuristics (or a custom `isEntityType` function) * and converted to `DoctypeMeta`-compatible JSON objects. * * @param source - GraphQL introspection result or SDL string * @param options - Conversion options for controlling output format and behavior * @returns Array of converted Stonecrop doctype definitions * * @example * ```typescript * // From introspection result (fetched from any GraphQL server) * const introspection = await fetchIntrospection('http://localhost:5000/graphql') * const doctypes = convertGraphQLSchema(introspection) * * // From SDL string * const sdl = fs.readFileSync('schema.graphql', 'utf-8') * const doctypes = convertGraphQLSchema(sdl) * * // With PostGraphile custom scalars * const doctypes = convertGraphQLSchema(introspection, { * customScalars: { * BigFloat: { component: 'ANumericInput' } * } * }) * ``` * * @public */ export declare function convertGraphQLSchema(source: IntrospectionSource, options?: GraphQLConversionOptions): ConvertedGraphQLDoctype[]; /** * Custom fetch strategy - uses a custom handler function. * @public */ export declare const CustomFetch: z.ZodObject<{ method: z.ZodLiteral<"custom">; handler: z.ZodString; }, z.core.$strip>; /** * Custom fetch strategy type * @public */ export declare type CustomFetch = z.infer; /** * Interface for data clients that fetch doctype metadata and records. * Implemented by \@stonecrop/graphql-client's StonecropClient. * Custom implementations can use any backend (REST, local storage, etc.). * * @typeParam T - Doctype reference type for record operations (defaults to DoctypeRef) * @typeParam M - Doctype metadata return type for getMeta (defaults to DoctypeMeta) * @public */ export declare interface DataClient { /** * Fetch doctype metadata * @param context - Doctype context identifying the doctype * @returns Doctype metadata or null if not found */ getMeta(context: DoctypeContext): Promise; /** * Fetch a single record by ID * * When `includeNested` is set, builds a query with sub-selections for descendant * links and returns ancestor + merged descendants. When omitted, returns flat scalar data. * * @param doctype - Doctype reference (name and optional slug) * @param recordId - Record ID to fetch * @param options - Query options * @returns Record data wrapped in GetRecordResult */ getRecord(doctype: T, recordId: string, options?: GetRecordOptions): Promise; /** * Fetch a page of records * @param doctype - Doctype reference (name and optional slug) * @param options - Query options * @returns The page, plus whether more exist and (on request) the total */ getRecords(doctype: T, options?: GetRecordsOptions): Promise; /** * Execute a doctype action (e.g., SUBMIT, APPROVE, save). * All state changes flow through this single mutation endpoint. * * @param doctype - Doctype reference (name and optional slug) * @param action - Action name to execute (e.g., 'SUBMIT', 'APPROVE', 'save') * @param args - Action arguments (typically record ID and/or form data) * @returns Action result with success status, response data, and any error */ runAction(doctype: T, action: string, args?: unknown[]): Promise<{ success: boolean; data: unknown; error: string | null; }>; } /** * Default heuristic to filter fields on entity types. * Skips internal fields that don't represent meaningful data. * * @param fieldName - The GraphQL field name * @param _field - The GraphQL field definition (unused in default implementation) * @param parentType - The parent entity type, whose interfaces declare its Relay identifier * @returns `true` if this field should be included * @public */ export declare function defaultIsEntityField(fieldName: string, _field: GraphQLField, parentType: GraphQLObjectType): boolean; /** * Default heuristic to determine if a GraphQL object type represents an entity. * An entity type becomes a Stonecrop doctype. * * This heuristic excludes: * - Introspection types (`__*`) * - Root operation types (`Query`, `Mutation`, `Subscription`) * - Types with synthetic suffixes (e.g., `*Connection`, `*Edge`, `*Input`) * - Types starting with `Node` interface marker (exact match only) * * @param typeName - The GraphQL type name * @param type - The GraphQL object type definition * @returns `true` if this type should become a Stonecrop doctype * @public */ export declare function defaultIsEntityType(typeName: string, type: GraphQLObjectType): boolean; /** * Context for identifying what doctype/record we're working with. * Used by graphql-middleware and graphql-client to resolve schema metadata. * @public */ export declare interface DoctypeContext { /** Doctype name (e.g., 'Task', 'Customer') */ doctype: string; /** Optional record ID for viewing/editing a specific record */ recordId?: string; /** Additional context properties */ [key: string]: unknown; } /** * What generation found that the authored doctype does not agree with. Every bucket is advisory — * nothing here is applied automatically. * * @public */ export declare interface DoctypeDrift { /** The authored doctype's name. */ doctype: string; /** * `clean` — the authored primary key is the one generation would derive. * `partial` — the doctype declares an identity generation cannot derive, so identity was left alone. */ mode: 'clean' | 'partial'; /** Why the mode is `partial`, when it is. */ reason?: string; /** Fieldnames confirmed against the schema and stamped. */ tagged: string[]; /** Authored fields with no matching schema field — app components, fieldsets, or stale entries. */ orphan: string[]; /** Schema fields absent from the doctype. Usually deliberate curation, occasionally an oversight. */ omitted: string[]; /** `fieldname: authored=… schema=…` where the chosen component differs from the scalar mapping. */ componentDrift: string[]; /** `fieldname: authored=… schema=…` where nullability disagrees. */ requiredDrift: string[]; /** Identity properties that differ. These are the ones a human must adjudicate. */ identityDrift: string[]; } /** * Union of all authoring-time field variants. * Use `kind` to discriminate: `'field'` | `'fieldset'` | `'table'`. * @public */ export declare type DoctypeField = ValueField | FieldsetField | TableField; /** * Zod runtime validation schema for the DoctypeField discriminated union. * Validates all three field variants: `'field'`, `'fieldset'`, `'table'`. * @public */ export declare const DoctypeFieldSchema: z.ZodType>; /** * Doctype metadata - complete definition of a doctype * @public */ export declare const DoctypeMeta: z.ZodObject<{ name: z.ZodString; slug: z.ZodOptional; displayField: z.ZodOptional; route: z.ZodOptional; fields: z.ZodArray>>; links: z.ZodOptional; backlink: z.ZodOptional; component: z.ZodOptional; fieldname: z.ZodOptional; fetch: z.ZodOptional; limit: z.ZodOptional; }, z.core.$strip>, z.ZodObject<{ method: z.ZodLiteral<"lazy">; }, z.core.$strip>, z.ZodObject<{ method: z.ZodLiteral<"custom">; handler: z.ZodString; }, z.core.$strip>], "method">>; blockWorkflows: z.ZodOptional; }, z.core.$strip>>>; workflow: z.ZodOptional>; actions: z.ZodOptional>; allowedStates: z.ZodOptional>; nextState: z.ZodOptional; stateless: z.ZodOptional; selfTransition: z.ZodOptional; clientHandler: z.ZodOptional; }, z.core.$strip>>>; triggers: z.ZodOptional; on: z.ZodArray; clientHandler: z.ZodString; }, z.core.$strip>>>; layout: z.ZodOptional>; targetPosition: z.ZodOptional>; sourcePosition: z.ZodOptional>; }, z.core.$strip>>>; }, z.core.$strip>>; inherits: z.ZodOptional; }, z.core.$strip>; /** * Doctype metadata type inferred from Zod schema * @public */ export declare type DoctypeMeta = z.infer; /** * Base interface for doctype metadata passed to DataClient methods. * Only requires properties needed for record fetching. * @public */ export declare interface DoctypeRef { /** Doctype name (e.g., 'Task', 'Customer') */ name: string; /** URL-friendly slug (e.g., 'task', 'customer') */ slug?: string; } /** * Fetch strategy for link data loading. * - sync: fetched in the initial query * - lazy: fetched on demand in a separate query * - custom: uses a custom handler function * @public */ export declare const FetchStrategy: z.ZodDiscriminatedUnion<[z.ZodObject<{ method: z.ZodLiteral<"sync">; limit: z.ZodOptional; }, z.core.$strip>, z.ZodObject<{ method: z.ZodLiteral<"lazy">; }, z.core.$strip>, z.ZodObject<{ method: z.ZodLiteral<"custom">; handler: z.ZodString; }, z.core.$strip>], "method">; /** * Fetch strategy type * @public */ export declare type FetchStrategy = z.infer; /** * Field options - flexible bag for type-specific configuration. * * Usage: * - Select: array of choices (["Draft", "Submitted", "Cancelled"]) * - Select with badges: \{ choices: [...], badges: \{ Open: "warning", ... \} \} or bare map * - Decimal: config object (\{ precision: 10, scale: 2 \}) * - Code: config object (\{ language: "python" \}) * * Deliberately *not* a bare string: a string once meant "link target", which made the value's * shape encode its meaning. That job belongs to `ValueField.doctype`, leaving this a plain * choices-or-config bag. * * @public */ export declare const FieldOptions: z.ZodUnion, z.ZodRecord]>; /** * Field options type inferred from Zod schema * @public */ export declare type FieldOptions = z.infer; /** * A layout container that groups other fields. Resolves to a nested AForm. * @public */ export declare interface FieldsetField { /** Discriminator — identifies this as a fieldset container */ kind: 'fieldset'; /** Unique identifier for this fieldset within its doctype */ fieldname: string; /** Vue component to render this fieldset. Defaults to `'AFieldset'` in resolveSchema. */ component?: string; /** Human-readable label for the fieldset legend */ label?: string; /** Whether the fieldset can be collapsed */ collapsible?: boolean; /** Interaction mode for all children inside this fieldset */ mode?: InteractionMode; /** Nested field definitions — resolved recursively by resolveSchema */ schema: DoctypeField[]; } /** * Zod runtime validation schema for FieldsetField. * Recursive — FieldsetField.schema is validated against DoctypeFieldSchema. * @public */ export declare const FieldsetFieldSchema: z.ZodObject<{ kind: z.ZodLiteral<"fieldset">; fieldname: z.ZodString; component: z.ZodOptional; label: z.ZodOptional; collapsible: z.ZodOptional; mode: z.ZodOptional>; schema: z.ZodLazy>>>; }, z.core.$strip>; /** * The component a GraphQL scalar maps to. * * A one-property interface rather than a bare string so `customScalars` stays extensible: an * override is a `Partial`, and widening this later does not change that signature. * * @public */ declare interface FieldTemplate { /** The Vue component name to render fields of this scalar type (e.g. `'ATextInput'`). */ component: string; } /** * Validation configuration for form fields * @public */ export declare const FieldValidation: z.ZodObject<{ errorMessage: z.ZodString; }, z.core.$loose>; /** * Field validation type inferred from Zod schema * @public */ export declare type FieldValidation = z.infer; /** * Recursively flatten Fieldset containers into a flat array of non-container fields. * Fieldset entries are replaced by their children; all other fields pass through. * * A fieldset is a layout grouping, not a scope: every field inside one is a field of the doctype, * with a column of its own and a name a link can bind to. Anything asking "what does this doctype * declare" must therefore descend, and the two ways to get that wrong point opposite ways — the * SELECT builder would omit real columns, while a validator would report a working declaration as * broken. * * Lives here rather than in the adapter because both sides need it: the middleware builds SQL from * it, and `DoctypeMeta`'s own validation asks the same question at the load gate. It sat in the * adapter while the validator hand-rolled a top-level-only scan, and that is exactly the second * failure this comment names — a `displayField` inside a fieldset was rejected at authoring time * and would have worked at runtime. * * A module of its own, importing nothing at runtime, because callers need the descent without * needing the rest of `field.ts` — which defines the Zod schemas, so a runtime edge to it is a * runtime edge to Zod. Zod reaching a Nitro SSR entry collides with the `process` Nitro imports * there and takes the server down with a `SyntaxError` per request, which no build reports. * `field.ts` still calls this and `index.ts` still exports it, so nothing outside moves. * * @param fields - the doctype's top-level fields * @returns every non-container field, fieldset children included * @public */ export declare function flattenFields(fields: readonly DoctypeField[]): (ValueField | TableField)[]; /** * Render a drift report as human-readable lines. Empty when generation agrees with the doctype. * * @param drift - a report from {@link mergeIntrospectedDoctype} * @returns one line per finding, ready to print * * @public */ export declare function formatDoctypeDrift(drift: DoctypeDrift): string[]; /** * One file the generator will write, and what that file is verified against. * * `basis` exists because the two are not always the same document. An aggregate is written from * its own one-field generation but verified against the **entity**, since its purpose is to carry * fewer columns than the table — checking it against itself reports every curated column as one * the table had dropped. * * @public */ export declare interface GenerationPlanEntry { /** The doctype to write. */ generated: ConvertedGraphQLDoctype; /** The doctype whose fields an existing file on disk is verified against. */ basis: ConvertedGraphQLDoctype; /** Whether the file is a curated subset of `basis` — passed through to `MergeOptions.subset`. */ subset: boolean; } /** Options for {@link planGeneration}. @public */ export declare interface GenerationPlanOptions { /** Emit only the entity doctypes, skipping their aggregates. Defaults to `false`. */ noAggregates?: boolean; /** Called with an advisory message for each entity that yields no aggregate. */ onWarning?: (message: string) => void; /** * Identity the authored doctype on disk declares, keyed by doctype `name`. * * SDL cannot say which `UNIQUE` column is a table's key, so for a natural-key table the converter * derives nothing and the answer exists only in the file. Without this the aggregate is * unreachable: generation says "declare a primaryKey and re-run", and re-running after declaring * one changes nothing, because planning never reads the file. * * Passed in rather than read here so this stays a pure function of its inputs; the CLI owns the * IO. The plan is then a function of the schema *and* what is already on disk. */ identity?: Record; } /** * Resolve the field a doctype nominates as its display text, or `undefined` when the nomination * does not name a readable column. * * This is the single definition of "is this a usable `displayField`". Both sides depend on it: * `DoctypeMeta` refuses a bad nomination at the load gate, and the adapter builds a SELECT from * the field it returns. Call this; never re-derive the rule, or the gate and the query will * disagree about which nominations are legal — which they did, in both directions at once. * * Two things disqualify a nomination, and both are the doctype saying so itself: * - it names no field at all, fieldset children included * - it names a `computed` field, which is declared precisely to state it has no column, so a * SELECT built from it would reference a column the database does not have * * @param fields - the doctype's top-level fields * @param displayField - the nominated fieldname * @returns the nominated field, or `undefined` when it is not a readable column * @public */ export declare function getDisplayField(fields: readonly DoctypeField[], displayField: string | undefined): ValueField | undefined; /** * The one string a doctype is addressed by. * * A doctype carries two names — `name` (`OrderItem`) and `slug` (`order-item`) — and every registry * must agree on which one keys it. Three implementations had drifted apart: the adapter's registry is * keyed by `name` and its `getMeta` also scans for a matching `slug`, so it accepts **either**; the * client's registry is keyed by a slug it derives itself and accepts **only** that; and * `Doctype.fromObject` dropped an authored `slug` on the floor and re-derived one regardless. The * adapter's accepted set was therefore a strict superset of the client's, and a link target written * as the Name booted the server, passed its reference check, served rows over GraphQL, and was * silently dropped by the client — an expanding child table rendering as one empty text input, with * nothing logged. * * Resolving through this in both runtimes is what makes the two answers the same answer. It is the * derivation only; a *lookup* still belongs to whichever registry owns the corpus, because the two * corpora legitimately differ (a client registers lazily, and a client-only host has no adapter at * all). * * An authored `slug` wins over the derived one because the authored doctype is the source of truth: * generation verifies a file and never overwrites it, so a doctype that states its own slug means it. * Deriving unconditionally is what `fromObject` did, and it made an authored `slug` a silent no-op on * one side of the wire while the other honoured it. * * @param doctype - anything carrying a doctype's `name` and optional authored `slug` * @returns the canonical slug * @public * * @example * ```typescript * getDoctypeSlug({ name: 'OrderItem' }) // 'order-item' * getDoctypeSlug({ name: 'Planner', slug: 'planner-board' }) // 'planner-board' * ``` */ export declare function getDoctypeSlug(doctype: { name: string; slug?: string; }): string; /** * Find the field a doctype marks as its primary key, or `undefined` when none is marked. * * This is the single definition of "which field identifies a record". Both sides depend on it: * the middleware builds the SQL identity predicate from it, and the client resolves a record's * route/store key from it. Call this; never re-derive the rule at the call site, or the two will * drift and the client will key records by a column the server never queried. * * Two deliberate rules, both matching the shape `primaryKey` actually has: * - Fieldset children are **included**, via {@link flattenFields}. A fieldset is layout, not * scope: its children are fields of the doctype with columns of their own, which is why the * adapter's SELECT already descends and why `getDisplayField` does too. Scanning top level only * did not *refuse* a nested declaration — it ignored one, so an author marked identity and * nothing honoured it and nothing said so. * - The **first** match in document order wins. Identity is single-valued by design — a doctype * describes the API surface, and mapping a composite database key onto one identity there is the * adapter's job — so a doctype declaring several is malformed rather than composite. * `DoctypeMeta` rejects that at the load gate; this stays total for callers holding fields that * never went through it. * * @param fields - the doctype's fields; fieldset children are descended into * @returns the primary-key field, or `undefined` for a PK-less doctype * @public */ export declare function getPrimaryKeyField(fields: readonly DoctypeField[]): ValueField | undefined; /** * Resolve a record's identity value using the doctype's declared primary key. * * Falls back to `record.id` when the doctype declares no `primaryKey`. That fallback is * load-bearing, not defensive: surrogate-key doctypes carry an `id` column and never mark a * primary key, and PostGraphile renames a single-column `id` PK to `rowId` — so the declared * field and `id` are both real sources, in that order. * * @param fields - the doctype's top-level fields * @param record - the record to read the identity from * @returns the identity as a string, or `undefined` when neither source yields a usable value * @public */ export declare function getRecordIdentity(fields: readonly DoctypeField[], record: Record): string | undefined; /** * The name of the field a record is identified by: the declared `primaryKey`, or `id` when the * doctype declares none. * * The `id` fallback is load-bearing, not defensive — a surrogate-key doctype carries an `id` * column and marks no primary key, so "nothing declared" means `id`, not "no identity". * * This exists because that one-line rule had been restated at four sites — the client's * `Doctype.recordIdField`, both nuxt hosts' `recordLookupField`, and the Postgres adapter — and * the fourth had omitted the fallback, so a doctype the client keyed by `id` was one the adapter * could not look up at all. Call this; a fifth restatement is how they diverge again. * * The returned name is not guaranteed to be a declared field: a doctype that declares no * `primaryKey` and no `id` yields `'id'` regardless. An adapter that must build a SQL predicate * from it has to confirm the field exists and say so when it does not, because selecting a column * the doctype never declared returns nothing rather than failing. * * @param fields - the doctype's top-level fields * @returns the identifying fieldname * @public */ export declare function getRecordIdField(fields: readonly DoctypeField[]): string; /** * Options for fetching a single record * @public */ export declare interface GetRecordOptions { /** * Include nested link sub-selections. * - `true`: include all descendant links * - `string[]`: include only named links * - `false` / omitted: scalar fields only (default) */ includeNested?: boolean | string[]; /** * Maximum depth for recursive sub-selections. * No default — unlimited when omitted. */ maxDepth?: number; } /** * Result from getRecord - includes the record data * @public */ export declare interface GetRecordResult { /** The record data, or null if not found */ record: Record | null; } /** * Options for fetching multiple records * @public */ export declare interface GetRecordsOptions { /** Filter expression (field-value pairs) */ filters?: Record; /** Order by expression (e.g. 'NAME_ASC') */ orderBy?: string; /** Maximum number of records to return */ limit?: number; /** Number of records to skip */ offset?: number; /** * Ask the backend for the total matching the filters as well as the page. * * Off by default because it costs a second query — a full scan on Postgres — and knowing * *whether* more exist (`hasMore`) is what a list view actually needs. Turn it on for a * "showing 20 of 4,312" style display. */ includeTotal?: boolean; } /** * Result from getRecords — a page of records, and enough to tell that it is one. * * A bare array used to be returned here, which claimed to be the whole collection. It is not: * a limit always applies, so a caller could not distinguish a complete list from a truncated * one. That is the entire reason this type exists. * * @public */ export declare interface GetRecordsResult { /** The records in this page */ data: Record[]; /** Whether the backend holds further records beyond this page */ hasMore: boolean; /** * Total records matching the filters, ignoring limit/offset. Present only when the caller * asked for it via {@link GetRecordsOptions.includeTotal} — counting is a full scan on most * backends, so it is never computed speculatively. */ count?: number; } /** * Mapping from standard GraphQL scalar types to Stonecrop field types. * These are defined by the GraphQL specification and are always available. * * @public */ export declare const GQL_SCALAR_MAP: Record; /** * Extended field metadata with optional GraphQL conversion metadata. * Only present when `includeUnmappedMeta` is enabled. * * @public */ export declare interface GraphQLConversionFieldMeta extends ValueField { /** Original GraphQL type name (for debugging/reference) */ _graphqlType?: string; /** Marks fields that couldn't be automatically mapped */ _unmapped?: boolean; /** Marks relationship fields that belong in `links`, not `fields` */ _isLink?: boolean; } /** * Options for converting a GraphQL schema to Stonecrop doctype schemas. * All hooks are optional — sensible defaults are provided for common GraphQL patterns. * * @public */ export declare interface GraphQLConversionOptions { /** * GraphQL type names to exclude from conversion. * Applied after `isEntityType` filtering. */ exclude?: string[]; /** * Whitelist of GraphQL type names to convert. * When provided, only these types are considered (after `isEntityType` filtering). */ include?: string[]; /** * Emit a doctype under a different name than its GraphQL type. Key is the GraphQL type name, * value is the doctype `name`; `slug` is derived from the value. * * This exists for the case where a doctype is not one-to-one with a table — a second view over * an existing type, say, distinguished only by presentation. Without it the converter can only * ever name a doctype after its type. * * Keep it consistent with the middleware's `tables` option, which maps the resulting doctype * name to its SQL target. * * @example * ```typescript * { Plan: 'Planner' } // emits a doctype named Planner, slug 'planner', from type Plan * ``` */ doctypeNames?: Record; /** * Called with any advisory message raised during conversion — currently only the * un-normalized-PostGraphile warning. Left to the caller so the library never writes to the * console itself. */ onWarning?: (message: string) => void; /** * Map custom or non-standard GraphQL scalar types to the component that renders them. * Merged with the built-in scalar maps (GQL_SCALAR_MAP + WELL_KNOWN_SCALARS). * User-provided entries take highest precedence. * * @example * ```typescript * { * MyCustomMoney: { component: 'ANumericInput' }, * PostGISPoint: { component: 'ATextInput' } * } * ``` */ customScalars?: Record>; /** * Custom function to determine if a GraphQL object type represents an entity (→ doctype). * When provided, replaces the default heuristic entirely. * * The default heuristic excludes types matching synthetic patterns: * `*Connection`, `*Edge`, `*Input`, `*Patch`, `*Payload`, `*Condition`, * `*Filter`, `*OrderBy`, `*Aggregate`, `Query`, `Mutation`, `Subscription`, `__*`. * * @param typeName - The GraphQL type name * @param type - The full GraphQL object type definition * @returns `true` if this type should become a Stonecrop doctype */ isEntityType?: (typeName: string, type: GraphQLObjectType) => boolean; /** * Custom function to filter which fields on an entity type are included. * When provided, replaces the default field filter. * * The default filter excludes `nodeId`, `__typename`, and `clientMutationId`. * * @param fieldName - The GraphQL field name * @param field - The full GraphQL field definition * @param parentType - The parent entity type * @returns `true` if this field should be included */ isEntityField?: (fieldName: string, field: GraphQLField, parentType: GraphQLObjectType) => boolean; /** * Escape hatch: fully override the classification of a specific field. * When this returns a non-null value, it is used as the field definition * (merged with the field name). Return `null` to fall through to default classification. * * @param fieldName - The GraphQL field name * @param field - The full GraphQL field definition * @param parentType - The parent entity type * @returns Partial field meta to use, or `null` for default behavior */ classifyField?: (fieldName: string, field: GraphQLField, parentType: GraphQLObjectType) => Omit, 'kind'> | null; /** * Include `_graphqlType` and `_unmapped` metadata on converted fields. * Useful for debugging conversions. Defaults to `false`. */ includeUnmappedMeta?: boolean; } /** * Whether field options carry any badge mapping. * @public */ export declare function hasBadgeOptions(options: FieldOptions | undefined): boolean; /** * Which of the three field shapes an entry has, read from the entry's own structure. * * The single definition of that question. It had three copies before this — the parser's * `injectKind`, {@link stripFieldKind}'s agreement check, and the docbuilder's own * `isValueField` in another package — each free to drift, and drift here re-types a field rather * than throwing: a value field read as a fieldset loses its column, a fieldset read as a value * field loses every child. * * Deliberately **shape-only**: a declared `kind` is ignored. Two callers depend on that. The * stripper compares this against the declaration to decide whether removing it is lossless, which * it cannot do if this honours it. The docbuilder reads raw JSON off disk and classifies entries to * decide which to render as editable rows — and `kind` is Stonecrop's own discriminant, not * something a doctype author writes, so a tool reading a file has no business consulting it. * * `injectKind` is the one place a declaration still wins, and only to leave an already-parsed * object untouched on its way back through. * * @param field - a field entry, authored or parsed * @returns the kind its shape implies * @public */ export declare function inferFieldKind(field: unknown): DoctypeField['kind']; /** * Controls the level of user interaction for a field, container, or table. * * - `'edit'` — field is fully interactive; user can change the value * - `'read'` — field is non-interactive but displayed with form chrome (input outline, etc.) * - `'display'` — field is non-interactive and displayed as plain text; no form chrome * * Applied at authoring time via `mode` on any `DoctypeField` variant. Propagated through * `resolveSchema()` into the resolved output types. Nested `AForm` and `ATable` components * inherit `mode` from their parent unless overridden at the field level. * * @public */ export declare type InteractionMode = 'edit' | 'read' | 'display'; /** * Set of scalar type names that are internal to GraphQL servers and should be skipped * during field conversion (they don't represent meaningful data fields). * * @public */ export declare const INTERNAL_SCALARS: Set; /** * The field properties a `source: 'introspected'` marker freezes — the ones the database owns. * * This is the single definition of the identity set. The docbuilder greys these inputs on an * introspected field, and the converter's merge refuses to rewrite them. Stating it twice is how * the two drift, so both read this constant. * * Everything absent from this list is author-owned, `component` most importantly: it chooses the * widget, which is an authoring decision the database has no opinion about. * * @public */ export declare const INTROSPECTED_IDENTITY_PROPS: readonly ["fieldname", "primaryKey", "required", "options", "cardinality", "doctype"]; /** * Input source for the GraphQL schema converter. * Accepts either a standard GraphQL introspection result or an SDL string. * * - `IntrospectionQuery`: The raw result of a GraphQL introspection query (from any server) * - `string`: An SDL (Schema Definition Language) string * * Note: URL fetching is intentionally not supported in the library API. * Use the CLI (`stonecrop-schema generate --endpoint `) for endpoint fetching, * or fetch the introspection result yourself and pass it in. * * @public */ export declare type IntrospectionSource = IntrospectionQuery | string; /** * Whether a workflow action may run from `currentState`. * * Single source of truth for the "is this action available here" rule, shared by * the frontend (`getAvailableTransitions`) and the server-side dispatch guard so * the two can never disagree. Empty or absent `allowedStates` means the action is * available in ALL states — a plain `allowedStates.includes(currentState)` would * wrongly block such actions everywhere. * * @public */ export declare function isActionAllowedInState(action: { allowedStates?: string[] | null; }, currentState: string): boolean; /** * True when `value` is a resolved badge descriptor for ACell / ADropdown. * @public */ export declare function isBadgeDescriptor(value: unknown): value is BadgeDescriptor; /** * True when `options` is a Select choice map (`{ Open: "warning", ... }` or * `{ choices: [...], badges: {...} }`), not a quantity/currency/code config bag. * @public */ export declare function isSelectChoiceMap(options: FieldOptions | undefined): options is Record; /** * True when `options` uses the structured SelectOptions shape. * @public */ export declare function isSelectOptions(options: FieldOptions | undefined): options is SelectOptions; /** * Lazy fetch strategy - data is fetched on demand in a separate query. * @public */ export declare const LazyFetch: z.ZodObject<{ method: z.ZodLiteral<"lazy">; }, z.core.$strip>; /** * Lazy fetch strategy type * @public */ export declare type LazyFetch = z.infer; /** * Suffix appended to a link fieldname for its pre-resolved display text in record payloads. * * @deprecated The `__display` suffix pattern is no longer used. Inline link fields are enriched * server-side by `@stonecrop/graphql-middleware` as `{ id, displayText }` objects on the link * field itself. * @public */ export declare const LINK_DISPLAY_SUFFIX = "__display"; /** * Link declaration - describes a relationship from one doctype to another. * @public */ export declare const LinkDeclaration: z.ZodObject<{ target: z.ZodString; cardinality: z.ZodEnum<{ atMostOne: "atMostOne"; one: "one"; noneOrMany: "noneOrMany"; atLeastOne: "atLeastOne"; }>; backlink: z.ZodOptional; component: z.ZodOptional; fieldname: z.ZodOptional; fetch: z.ZodOptional; limit: z.ZodOptional; }, z.core.$strip>, z.ZodObject<{ method: z.ZodLiteral<"lazy">; }, z.core.$strip>, z.ZodObject<{ method: z.ZodLiteral<"custom">; handler: z.ZodString; }, z.core.$strip>], "method">>; blockWorkflows: z.ZodOptional; }, z.core.$strip>; /** * Link declaration type inferred from Zod schema * @public */ export declare type LinkDeclaration = z.infer; /** * Build the payload key for a link field's display text (e.g. `customerId__display`). * * @deprecated The `__display` suffix pattern is no longer used. Inline link fields are enriched * server-side by `@stonecrop/graphql-middleware` as `{ id, displayText }` objects on the link * field itself. * @public */ export declare function linkDisplayFieldname(fieldname: string): string; /** * Whether a link component expands its target doctype, or renders the link inline. * * This is the *only* axis the component decides. It deliberately does not choose between an * embedded record and an embedded table: `cardinality` states whether the value is a scalar or * an array, which is a fact about the data rather than a rendering preference, so a component * must not be able to override it (an `AForm` over a `noneOrMany` link would be handed an array * it cannot render). Component names encode both axes — `AFormLink`/`ATableLink` are the inline * pair, `AForm`/`ATable` the expanding pair — but only the inline/expand half is authoritative. * * @public */ export declare type LinkExpansion = 'inline' | 'expand'; /** * How a link field renders. * * - `inline` — a scalar id-picker; the target is *not* expanded (the field keeps its own value * and carries a `doctype` prop for async display-text resolution and navigation). * - `record` — the target doctype is resolved and embedded as a nested form. * - `table` — the target doctype is resolved and embedded as a child table. * * @public */ export declare type LinkRenderMode = 'inline' | 'record' | 'table'; /** * Resolve a stored choice value to a badge descriptor using field options. * @public */ export declare function lookupBadge(options: FieldOptions | undefined, key: string | undefined): BadgeDescriptor | undefined; /** * Verify an authored doctype against freshly generated output and stamp provenance. * * @param authored - the doctype as it exists on disk; every key not named below is preserved verbatim * @param generated - `convertGraphQLSchema` output for the corresponding GraphQL type. For a * `subset` merge this is the **entity**, whose fields are the set the subset is curated from * @param options - see {@link MergeOptions} * @returns the doctype to write, plus a drift report * * @example * ```ts * const [generated] = convertGraphQLSchema(introspection, { include: ['Uom'] }) * const { doctype, drift } = mergeIntrospectedDoctype(JSON.parse(onDisk), generated) * if (drift.identityDrift.length) console.warn(drift.identityDrift.join('\n')) * ``` * * @public */ export declare function mergeIntrospectedDoctype(authored: AuthoredDoctype, generated: ConvertedGraphQLDoctype, options?: MergeOptions): MergeResult; /** * How to verify the authored doctype against the schema. * * @public */ export declare interface MergeOptions { /** * The authored doctype is a curated **subset** of the schema's columns rather than a model of * all of them — an aggregate being the case this exists for. * * This changes what counts as drift in both directions, so `generated` must be passed the * *entity's* full field set, not the subset's. A column the author added to an aggregate is * then confirmed against the real table (so a genuinely dropped column still reports as an * orphan), while the columns deliberately left out stop reporting as omissions. Without it an * aggregate reports phantom drift on every run, which both spams `--check` and buries the one * finding that matters. */ subset?: boolean; } /** Outcome of a merge: the doctype to write, plus what generation disagreed with. @public */ export declare interface MergeResult { /** The authored doctype with `source` markers added and nothing else changed. */ doctype: AuthoredDoctype; /** Advisory report. Never applied. */ drift: DoctypeDrift; } /** * Recursively injects the `kind` discriminant into a raw field object and, for fieldsets, * into each of its nested `schema` children — mirroring exactly what Zod's `preprocess` * does at every level of the discriminated union. * * Table `columns` are {@link ColumnSchema} entries, not `DoctypeField`s, so they are left * untouched — the Zod table schema validates them with a plain passthrough and never injects * `kind` there either. * * Needed because `Doctype.fromObject` constructs a Doctype without running Zod, yet the * registry's `resolveFields` gates link and fieldset handling on `field.kind`. Without this, * a JSON-authored link resolves to a flat scalar and a fieldset's children are dropped. * * @public */ export declare function normalizeFieldKind(field: unknown): unknown; /** * Parse and validate a doctype, throwing on failure * @param data - Data to parse * @returns Validated DoctypeMeta * @throws ZodError if validation fails * @public */ export declare function parseDoctype(data: unknown): DoctypeMeta; /** * Parse and validate a field, throwing on failure * @param data - Data to parse * @returns Validated DoctypeField * @throws ZodError if validation fails * @public */ export declare function parseField(data: unknown): DoctypeField; /** * Convert PascalCase to snake_case (e.g., for deriving table names from type names) * @param pascal - PascalCase string * @returns snake_case string * @public * @example * ```typescript * pascalToSnake('SalesOrder') // 'sales_order' * pascalToSnake('SalesOrderItem') // 'sales_order_item' * ``` */ export declare function pascalToSnake(pascal: string): string; /** * Expand converted entities into the set of doctype files to write. * * Each table yields two: the entity, whose fields carry every column and which backs the record * form, and its aggregate — the collection view. They are written as peers, one file each, with * no key relating them. * * Separate from the CLI because the pairing of a file to its verification basis is the part that * is easy to get wrong and impossible to notice: getting it wrong does not throw, it just reports * drift that is not there, forever. * * @param entities - `convertGraphQLSchema` output * @param options - see {@link GenerationPlanOptions} * @returns one entry per file to write * @public */ export declare function planGeneration(entities: readonly ConvertedGraphQLDoctype[], options?: GenerationPlanOptions): GenerationPlanEntry[]; /** * Decide how a *declared* link (one with a `LinkDeclaration`) renders. * * Two independent axes: the **component** picks inline vs expand, and when expanding the * **cardinality** picks record vs table (many → table). The declaration's component wins over the * field's, matching the precedence the resolver already uses for the rendered component. * * This is the single definition of "does this link expand" — it is consumed by both the client * resolver (which builds the nested schema) and the server column builder (which must still * SELECT an `inline` link's FK column). Call it; never re-derive the rule at the call site, or * the two will drift and the client will render a table for a column the server never selected. * * @param link - the link declaration (only `component` and `cardinality` are consulted) * @param fieldComponent - the linked field's own `component`, used when the declaration names none * @public */ export declare function resolveLinkRenderMode(link: { component?: string; cardinality?: string; }, fieldComponent?: string): LinkRenderMode; /** * Dropdown / filter choice strings. * @public */ export declare function selectChoices(options: FieldOptions | undefined): string[]; /** * Select field options when choices carry badge colors. * @public */ export declare interface SelectOptions extends Record { choices: string[]; badges?: Record; } /** * Serialized function type - a function serialized to a string. * Used for custom fetch handlers. * @public */ export declare type SerializedFunction = string; /** * Converts snake_case to camelCase * @param snakeCase - Snake case string * @returns Camel case string * @public * @example * ```typescript * snakeToCamel('user_email') // 'userEmail' * snakeToCamel('created_at') // 'createdAt' * ``` */ export declare function snakeToCamel(snakeCase: string): string; /** * Converts snake_case to Title Case label * @param snakeCase - Snake case string * @returns Title case label * @public * @example * ```typescript * snakeToLabel('user_email') // 'User Email' * snakeToLabel('first_name') // 'First Name' * ``` */ export declare function snakeToLabel(snakeCase: string): string; /** * Remove the `kind` discriminant from a field, recursing into a fieldset's children. * * The outbound half of the boundary {@link normalizeFieldKind} owns inbound. `kind` is a * discriminated-union tag the parser synthesizes, not something an author writes, so nothing that * *writes* a doctype should put it on disk — the generator and the docbuilder's save both call * this. Without it the two round-trip asymmetrically: every save adds a key the file never had. * * Strips only when `injectKind` would restore exactly what was removed. A fieldset carrying no * `schema` re-infers as a plain field, so its `kind` is kept rather than silently re-typing the * document; `DoctypeMeta` requires `schema` on a fieldset, so that shape is already invalid and * belongs to the load gate, not here. * * Table `columns` are {@link ColumnSchema} entries rather than `DoctypeField`s and never carry an * injected `kind`, so they are passed through untouched — the same asymmetry `injectKind` has. * * @param field - a field object, as held in memory after parsing * @returns the field without `kind`, safe to serialize * @public */ export declare function stripFieldKind(field: unknown): unknown; /** * Sync fetch strategy - data is fetched in the initial query. * @public */ export declare const SyncFetch: z.ZodObject<{ method: z.ZodLiteral<"sync">; limit: z.ZodOptional; }, z.core.$strip>; /** * Sync fetch strategy type * @public */ export declare type SyncFetch = z.infer; /** * An inline table whose columns are defined directly in the schema (no linked doctype). * Use when the table data does not warrant a separate doctype. * @public */ export declare interface TableField { /** Discriminator — identifies this as an inline table */ kind: 'table'; /** Unique identifier for this table within its doctype */ fieldname: string; /** Vue component to render this table. Defaults to `'ATable'` in resolveSchema. */ component?: string; /** Human-readable label */ label?: string; /** Column definitions — use ColumnSchema (fieldname key) from \@stonecrop/schema */ columns: ColumnSchema[]; /** View configuration — defaults to `{ view: 'list' }` in resolveSchema when absent */ config?: TableViewConfig; /** Interaction mode for all cells inside this table */ mode?: InteractionMode; } /** * Zod runtime validation schema for TableField. * @public */ export declare const TableFieldSchema: z.ZodObject<{ kind: z.ZodLiteral<"table">; fieldname: z.ZodString; component: z.ZodOptional; label: z.ZodOptional; columns: z.ZodArray>; config: z.ZodOptional>; fullWidth: z.ZodOptional; defaultTreeExpansion: z.ZodOptional>; dependencyGraph: z.ZodOptional; }, z.core.$strip>>; mode: z.ZodOptional>; }, z.core.$strip>; /** * JSON-safe view configuration for table fields in doctype authoring. * * This is the authoring-time subset of `@stonecrop/atable`'s `TableConfig`. It covers * the view discriminator and structural options that can be expressed in static JSON. * `rowActions` (which requires function-typed handlers) stays in the runtime `TableConfig`. * * @public */ export declare const TableViewConfig: z.ZodObject<{ view: z.ZodOptional>; fullWidth: z.ZodOptional; defaultTreeExpansion: z.ZodOptional>; dependencyGraph: z.ZodOptional; }, z.core.$strip>; /** * Table view configuration type inferred from Zod schema * @public */ export declare type TableViewConfig = z.infer; /** * Convert table name to PascalCase doctype name * @param tableName - SQL table name (snake_case) * @returns PascalCase name * @public */ export declare function toPascalCase(tableName: string): string; /** * Convert to kebab-case slug * @param name - Name to convert * @returns kebab-case slug * @public */ export declare function toSlug(name: string): string; /** * Reactive field-validation trigger — advisory, client-side only. * * A Trigger is a docbuilder-authored validator: when any field in `on` is edited, its * `clientHandler` runs (client-side, no rollback) and may flag a field inline to block save * in the UI. It is deliberately a **sibling** to {@link (ActionDefinition:type)}, not a member of it — * a reactive validator is not a user-invoked action, so it lives in the `triggers` map on * {@link (WorkflowMeta:type)} and never appears to action readers (transition/command dropdowns, the FSM graph). * * The two bindings are independent: `on` is the fire-set (which fields' edits run it), while the * `setError(field, msg)` call inside `clientHandler` chooses which field displays the error. * @public */ export declare const TriggerDefinition: z.ZodObject<{ label: z.ZodOptional; on: z.ZodArray; clientHandler: z.ZodString; }, z.core.$strip>; /** * Trigger definition type inferred from Zod schema * @public */ export declare type TriggerDefinition = z.infer; /** * Reduce a record's *inline* link values to the ids that get persisted. * * The adapter returns an inline link as `{ id, displayText }`, so that is what a record holds * everywhere it is read — the store, a list row, a form field. A column takes the id alone, so * this is the single definition of the shape a record leaves in, and it belongs at the boundary * a record crosses on its way to the server, never on the way into the store. * * Doing it on the way in destroys the text the adapter looked up: nothing else holds it, so the * field that resolved a moment ago renders its raw id, and the same record then renders * differently depending on whether anything had edited the form yet. That is the bug this exists * to prevent, and its damage is a wrong render, not a throw. * * Only *inline* links may be reduced. An inline link's value is indistinguishable by inspection * from an expanded one (`{ id, ...the whole target record }`), so `component` — which states * which of the two a field is — is what tells them apart, via {@link componentLinkExpansion}. * Reducing an expanded link would send the id in place of the record. * * Fieldsets are descended into in both shapes a record appears in: flat, as the store and the * server hold it, and nested under the fieldset's own key, as a form emits it. * * @param fields - the doctype's top-level fields * @param record - the record to reduce; not mutated * @returns a shallow copy with every inline link reduced to its id * @public */ export declare function unwrapInlineLinks(fields: readonly DoctypeField[], record: Record): Record; /** * Validate a doctype definition * @param data - Data to validate * @returns Validation result * @public */ export declare function validateDoctype(data: unknown): ValidationResult; /** * Validate a field definition against the DoctypeField discriminated union * @param data - Data to validate * @returns Validation result * @public */ export declare function validateField(data: unknown): ValidationResult; /** * Validation error with path information * @public */ export declare interface ValidationError { /** Path to the invalid property */ path: PropertyKey[]; /** Error message */ message: string; } /** * Result of a validation operation * @public */ export declare interface ValidationResult { /** Whether validation passed */ success: boolean; /** List of validation errors (empty if success) */ errors: ValidationError[]; } /** * A field that holds a scalar value, a link to another record, or a select choice. * The most common kind of field. `component` determines how it renders; the attributes below * carry everything else that is not a rendering concern. * @public */ export declare interface ValueField { /** Discriminator — identifies this as a value-holding field */ kind: 'field'; /** Unique identifier for this field within its doctype */ fieldname: string; /** * Vue component that renders this field — the primary (and only) rendering axis. Required: * there is nothing left to derive it from, and a field without one has nothing to render it. * Any string is valid; naming a custom component is how an app renders a field Stonecrop * ships no widget for. See `CANONICAL_COMPONENTS` for the set Stonecrop provides. */ component: string; /** True for the field that identifies the record's primary-key column. */ primaryKey?: boolean; /** True for a computed/display field with no backing DB column — excluded from SQL SELECT. */ computed?: boolean; /** Editor language for code fields (e.g. `'json'`, `'typescript'`) — the only thing distinguishing * a JSON editor from a code editor, since both render with `ACodeEditor`. */ language?: string; /** * Target doctype slug. Presence is what makes a field a link. * * How it renders is decided by `component`, not by this: `AFormLink` renders an * inline id-picker, while `AForm`/`ATable` expand the target (see `linkRenderMode`). Expansion * metadata — backlink, fetch strategy, authoritative cardinality — lives in the doctype's * `links` map, which is additive and never required for a plain foreign key. */ doctype?: string; /** Human-readable label */ label?: string; /** CSS width (e.g. `"40ch"`, `"200px"`) */ width?: string; /** CSS height (e.g. `"100%"`, `"40vh"`) — used by full-viewport fields such as Planner */ height?: string; /** Text alignment */ align?: 'left' | 'center' | 'right' | 'start' | 'end'; /** Whether the field is editable in table cell context */ edit?: boolean; /** Input mask pattern or serialized function */ mask?: string; /** Serialized display formatter — distinct from `mask` (input). Spreads through * `schemaToColumns` to `ColumnSchema.format`; deserialized at render time by ATable's * `getFormattedValue`. Returns a plain string, HTML, or a {@link BadgeDescriptor} for badge * cells. When a descriptor is returned it wins over any badge map on `options`. */ format?: string; /** Per-field interaction mode override */ mode?: InteractionMode; /** Type-specific options: Select choices, Decimal precision config, etc. A link's target is not * here — it is `doctype`. */ options?: FieldOptions; /** Whether the field is required */ required?: boolean; /** Whether the field is read-only */ readOnly?: boolean; /** Whether the field is hidden from the UI */ hidden?: boolean; /** Default value for new records */ default?: unknown; /** Validation configuration */ validation?: FieldValidation; /** Cardinality for Link fields — authoritative value on LinkDeclaration takes precedence */ cardinality?: 'atMostOne' | 'one' | 'noneOrMany' | 'atLeastOne'; /** * Provenance marker — stamped only by the GraphQL converter; absence means hand-authored. * When present, the docbuilder freezes the field's identity set (`fieldname`, `primaryKey`, * `required`, `options`, `cardinality`, `doctype`), since `fieldname` is the GraphQL/column * binding and `doctype` is the FK's target. `component` is deliberately **not** frozen: it * chooses the widget, which is an authoring decision the database has no opinion about. */ source?: 'introspected'; } /** * Zod runtime validation schema for ValueField. * @public */ export declare const ValueFieldSchema: z.ZodObject<{ kind: z.ZodLiteral<"field">; fieldname: z.ZodString; component: z.ZodString; primaryKey: z.ZodOptional; computed: z.ZodOptional; language: z.ZodOptional; doctype: z.ZodOptional; label: z.ZodOptional; width: z.ZodOptional; height: z.ZodOptional; align: z.ZodOptional>; edit: z.ZodOptional; mask: z.ZodOptional; format: z.ZodOptional; mode: z.ZodOptional>; options: z.ZodOptional, z.ZodRecord]>>; required: z.ZodOptional; readOnly: z.ZodOptional; hidden: z.ZodOptional; default: z.ZodOptional; validation: z.ZodOptional>; cardinality: z.ZodOptional>; source: z.ZodOptional>; }, z.core.$strip>; /** * Mapping from well-known custom GraphQL scalars to Stonecrop field types. * These cover scalars commonly used across GraphQL servers (PostGraphile, Hasura, etc.) * without baking in knowledge of any specific server. * * Entries here have lower precedence than `customScalars` from options, but higher * precedence than unknown/unmapped scalars. * * @public */ export declare const WELL_KNOWN_SCALARS: Record; /** * DocBuilder graph layout — node positions for the workflow-state graph, keyed by state name. * Pure authoring view-state: persisted in the doctype JSON so an author's manual arrangement * survives reloads, but — exactly like {@link (WorkflowMeta:type)}'s `triggers` — it is client-only * and never mirrored into the runtime GraphQL SDL (see the WorkflowMeta type in the host SDLs, which * expose only `states`/`actions`). The shape mirrors VueFlow's node fields; `position` is the node's * canvas coordinate and `targetPosition`/`sourcePosition` are the handle sides. * @public */ export declare const WorkflowLayout: z.ZodRecord>; targetPosition: z.ZodOptional>; sourcePosition: z.ZodOptional>; }, z.core.$strip>>; /** * Workflow layout type inferred from Zod schema * @public */ export declare type WorkflowLayout = z.infer; /** * Workflow metadata - states and actions for a doctype * @public */ export declare const WorkflowMeta: z.ZodObject<{ states: z.ZodOptional>; actions: z.ZodOptional>; allowedStates: z.ZodOptional>; nextState: z.ZodOptional; stateless: z.ZodOptional; selfTransition: z.ZodOptional; clientHandler: z.ZodOptional; }, z.core.$strip>>>; triggers: z.ZodOptional; on: z.ZodArray; clientHandler: z.ZodString; }, z.core.$strip>>>; layout: z.ZodOptional>; targetPosition: z.ZodOptional>; sourcePosition: z.ZodOptional>; }, z.core.$strip>>>; }, z.core.$strip>; /** * Workflow metadata type inferred from Zod schema * @public */ export declare type WorkflowMeta = z.infer; export { }