@atmyapp/structure
v0.1.1
Published
[](https://badge.fury.io/js/%40atmyapp%2Fstructure) [](https://opensource.org/licenses/ISC) [,
},
submissions: {
contact: defineSubmission({
description: "Main contact form",
fields: {
name: s.string(),
email: s.email(),
message: s.longText({
optional: true,
}),
resume: s.file({
optional: true,
}),
},
captcha: {
required: true,
provider: "hcaptcha",
},
}),
},
definitions: {
posts: defineCollection({
description: "Blog posts",
fields: {
title: s.string({
description: "Public title shown on the page",
}),
cover: s.image({
description: "Associated hero image for the post",
config: {
optimizeFormat: "webp",
maxSize: {
width: 1920,
height: 1080,
},
},
}),
body: s.mdx({
config: "blog",
description: "Long-form article body",
}),
author: s.reference({
target: "authors",
description: "Linked author profile",
by: "slug",
}),
seo: s.object({
description: "SEO metadata",
fields: {
title: s.string(),
socialImages: s.array({
optional: true,
description: "Nested list of social preview images",
items: s.image({
description: "Image used for social previews",
}),
}),
},
}),
},
systemFields: {
slug: {
enabled: true,
source: "title",
},
},
indexes: ["title"],
}),
settings: defineDocument({
description: "Single site settings file",
fields: {
theme: s.string({
description: "Theme identifier used by the UI",
}),
accent: s.string({
description: "Optional accent color",
optional: true,
}),
},
}),
},
});
const compiled = compileSchema(schema);defineDocument() does not need a path by default. If the key is settings, both settings and settings.json resolve to the same document automatically.
Neutral JSON input
The same canonical model can be authored without TypeScript:
{
"version": 1,
"description": "Marketing site schema",
"definitions": {
"settings": {
"kind": "document",
"description": "Single site settings file",
"path": "settings.json",
"fields": {
"theme": {
"kind": "scalar",
"scalar": "string",
"description": "Theme identifier used by the UI"
}
}
}
}
}Main APIs
Compiler
import { compileSchema, parseSchema, normalizeSchema } from "@atmyapp/structure";parseSchema(input): parses canonical JSON or legacy.structure.jsonnormalizeSchema(schema): normalizes definitions, paths, and defaultscompileSchema(input): builds the compiled schema with precomputed indexes
Introspection
import {
getDefinition,
getCollection,
getDocument,
getField,
listAssetFields,
listConfigs,
listReferences,
listSystemFields,
resolveDefinitionForPath,
} from "@atmyapp/structure";Use these helpers instead of manually traversing raw .structure objects.
Events
Analytics events are modeled as a top-level schema concern and preserved through canonical and legacy compilation:
import { defineBasicEvent, defineEvent, defineSchema } from "@atmyapp/structure";
const schema = defineSchema({
events: {
page_view: defineEvent(["page", "referrer", "timestamp"], {
description: "Ordered columns for page view analytics",
}),
session_start: defineBasicEvent({
description: "Marker event with dynamic payload",
}),
},
definitions: {},
});The compiled schema exposes these under compiled.events, and legacy .structure.json compatibility output preserves them under top-level events.
Submissions
Submissions are modeled as a first-class top-level schema concern:
import {
defineSchema,
defineSubmission,
s,
type InferSubmission,
} from "@atmyapp/structure";
const schema = defineSchema({
definitions: {},
submissions: {
contact: defineSubmission({
description: "Marketing contact form",
fields: {
name: s.string(),
email: s.email(),
message: s.longText({
optional: true,
}),
resume: s.file({
optional: true,
}),
},
captcha: {
required: true,
provider: "hcaptcha",
},
}),
},
});
type ContactSubmission = InferSubmission<"contact", typeof schema>;Notes:
acceptingResponsesis intentionally not part of the schema; that stays dashboard-managed- submission fields use the same field DSL as collections and documents
InferSubmissionis input-oriented, so asset fields infer upload values rather than stored CDN URLs- legacy compatibility output keeps captcha settings under
requiresCaptcha,captchaProvider, andhcaptchaSecret
References
References store string values by default, but the schema can describe how those strings should resolve:
s.reference({
target: "authors",
by: "id",
});
s.reference({
target: "posts",
by: "slug",
});
s.reference({
target: "settings",
by: "path",
});target: the referenced definition nameby: "id": default for collection-style identityby: "slug": requires the target to expose a slugby: "path": useful for document-style referencesmultiple: true: stores an array of referencesonDelete: optional lifecycle hint for downstream consumers
The current runtime value shape is a string or string array. That keeps references easy to store, easy to index, and easy to migrate while still carrying explicit semantics in the schema.
Nested types and lists
Nested objects and arrays are first-class in the canonical schema model:
const article = defineDocument({
fields: {
title: s.string(),
seo: s.object({
description: "SEO metadata",
fields: {
title: s.string(),
summary: s.string({ optional: true }),
},
}),
gallery: s.array({
optional: true,
items: s.image({
config: {
optimizeFormat: "webp",
},
}),
}),
},
});Notes:
- all nested fields are required unless marked with
optional: true - composite fields use the object-input style:
s.object({ fields, optional, description }),s.array({ items, optional, description }),s.reference({ target, ... }), ands.mdx({ config, ... }) s.array({ items: s.image() })works for image listss.gallery()is still available when you want a semantic “multiple images” asset field- nested field descriptions are preserved in the compiled schema and legacy output
Images and asset config
Image fields support config aligned with existing @atmyapp/core and @atmyapp/cli image definitions:
s.image({
config: {
optimizeFormat: "webp",
optimizeLoad: "progressive",
ratioHint: { x: 16, y: 9 },
maxSize: { width: 1920, height: 1080 },
},
});That config is preserved through canonical compilation and legacy .structure.json emission.
Validation
import {
validateSchemaDocument,
validateContent,
validateContentAtPath,
} from "@atmyapp/structure";validateSchemaDocument(input): validates authored schema documentsvalidateContent(compiled, definitionName, data): validates parsed content objectsvalidateContentAtPath(compiled, path, content, mimeType?): validates raw file content against the resolved definition
Migrations
import {
diffSchemas,
planMigration,
renderMigrationPrompts,
} from "@atmyapp/structure";Migration planning classifies changes as compatible, safe auto-convert, confirmable convert, or incompatible. The planner produces executable actions such as:
confirm_convertrequire_unionbackfill_generated_fieldcreate_unique_indexdrop_fielddrop_definition
Legacy .structure.json compatibility
The package can compile existing .structure.json files and also emit legacy-compatible output:
import { compileSchema, toLegacyStructure } from "@atmyapp/structure";
const compiled = compileSchema(existingStructureJson);
const legacy = toLegacyStructure(compiled.document);This keeps old projects working while runtime consumers migrate to the canonical schema model.
Development
npm ci
npm test
npm run build
npm pack --dry-runPublishing
The repository includes manual release workflows similar to the @atmyapp/core package:
- run tests before release
- verify the version was bumped
- build the package
- create a GitHub release
- publish to npm with provenance
Before publishing:
- Update the version in
package.json. - Run
npm test. - Run
npm run build. - Run
npm pack --dry-run. - Trigger the GitHub release workflow.
License
ISC
