Decoders for PXL Schema definitions and validation for PXL code generation framework
npm install @postxl/schemaA utility package that defines the schema for PXL projects & generators - together with the tooling to read/validate the schema.
A PXL project is defined by a "ProjectSchema" - typically through a JSON file.
The project schema contains:
- Overall project configuration (e.g. name, description)
- Definition of database schemas
- Configuration of generators
- Definition of models
- Definition of enums
The project schema currently defines the following field types:
- id: the primary key of the model
- scalar: a scalar value, e.g. string, int, boolean, date
- discriminated union (see below)
- relation: a relation to another model
- enum: an enum value
A discriminated union represents a a state that can be one of multiple values (differentiated by a discriminator value). E.g in case of a blog post, the author can be either a user or an anonymous user.
Both types might need an email address - but for the user we want to store the userId and for the anonymous user we want to store the name.
With this field, we can define the above states as follows:
``json`
{
"name": "AuthorType",
"type": "DiscriminatedUnion",
"commonFields": [
{
"name": "email",
"type": "String"
}
],
"members": [
{
"type": "User",
"label": "existing user",
"fields": [
{
"name": "userId",
"type": "User"
}
]
},
{
"type": "Anonymous",
"label": "Anonymous user",
"fields": [
{
"name": "name",
"type": "String"
}
]
}
]
}
The package exposes the following:
- type ProjectSchema: The overall project configuration. This includes the models, enums, generators, etc.type ProjectSchemaJSON
- : The JSON format of a ProjectSchemazProjectSchema
- : Zod decoder that converts ProjectSchemaJSON to a ProjectSchema
We use (Zod decoders)[https://zod.dev/] and transformers to convert from JSON to the final type and also perform validations.
For each element (Field, Model, Enum, ProjectSchema), we follow the following steps (and naming conventions):
1. Raw JSON decoder
- This decoder only performs type checking and provides descriptions for each property.
- Naming:
- Decoder: z
- Type:
- File:
2. Transform individual elements
- This transformation applies default values and branding to strings
- In case of FieldJSON, it also splits it into a discriminated union (Id, Scalar, RelationOrEnum)
- These transformations only require the decoded element from the previous step and are independent of the other elements.
\*Importantly, these transformations do not check any consistency with their parent elements.
- Naming:
- Type:
- Transformation:
- File:
3. Transform entire schema:
- Naming:
- Decoder: zProjectSchema (there is only this one decoder which combines all other decoders)
- Type:
- Transformation: , project-schema.transformer.ts
- Files:
Additionally, for each element, we provide the following files:
- : contains the default values for the element. To be used in tests and in the individual transformers
- : contains the branded names for the element
- : contains tests
`jsonisDefault
{
"name": "PostXL Demo project",
"slug": "postxl-demo",
"description": "A demo project for PostXL",
"version": "0.0.1",
"generators": {
"@PXL/core": {},
"@PXL/Types": {},
"@PXL/Backend/Data/Prisma": {
"prisma": {
"datasource": {
"name": "db",
"provider": "postgres",
"url": "env(\"DATABASE_CONNECTION\")"
},
"generator": {
"name": "client",
"provider": "prisma-client-js",
"previewFeatures": ["multiSchema"]
}
}
}
},
"systemUser": {
"id": "root",
"sub": null,
"email": "admin@postxl.com",
"name": "Root",
"profilePictureUrl": "https://postxl.com/images/team/user.png"
},
"standardModels": ["File", "Action", "Mutation", "Config"],
"models": [
{
"name": "User",
"description": "A user of the application.",
"schema": "PXL",
"standardFields": ["id", "createdAt", "updatedAt", "name"],
"fields": [
{
"name": "sub",
"type": "String?",
"isUnique": true,
"hasIndex": true,
"description": "The OpenID Connect provided subject identifier."
},
{
"name": "email",
"type": "String",
// validation: [
// {
// type: 'email',
// message: 'The email is not valid.',
// },
// ],
"description": "The email of the user."
// generation: 'faker("internet.email")',
},
{
"name": "profilePictureUrl",
"type": "String?",
"description": "The URL of the profile picture of the user."
// generation: 'faker("internet.avatar")',
}
],
"seed": [
{
"id": "System",
"name": "System",
"email": "admin@postxl.com",
"profilePictureUrl": "https://postxl.com/images/team/user.png"
},
{
"id": "User 1",
"name": "John Doe",
"email": "john.doe@company.com",
"profilePictureUrl": "https://postxl.com/images/team/user.png"
}
]
},
{
"name": "Country",
"description": "A lookup table, demonstrating the use of a @@IsDefault. See description of field for details.",`
"standardFields": ["id", "createdAt", "updatedAt"],
"labelField": "name",
"fields": [
{
"name": "countryCode",
"type": "String",
"description": "The country's ISO 3166-1 alpha-2 code.",
"isUnique": true
// generation: 'faker("address.countryCode")',
},
{
"name": "name",
"type": "String",
"description": "The name of the country.",
"isUnique": true
// generation: 'faker("address.country")',
},
{
"name": "isDefault",
"type": "Boolean?",
"description": "Tells that the country is the default country."
}
],
"defaultField": "isDefault",
"examples": [
{
"id": "Global",
"countryCode": "GLOBAL",
"name": "Global",
"isDefault": true
},
{
"id": "US",
"countryCode": "US",
"name": "United States"
},
{
"id": "DE",
"countryCode": "DE",
"name": "Germany"
}
]
},
{
"name": "Post",
"description": "A blog post entry.",
// fakerSeed: '12345',
"fields": [
{
"name": "body",
"type": "String",
"description": "The post that you should see after reading this one."
// generation: 'faker("lorem.paragraph")',
},
{
"name": "authorId",
"type": "User",
"description": "The user that authored the post.",
"prismaRelationFieldName": "author"
},
{
"name": "nextPostId",
"type": "Post?",
"description": "The next post that you should see after reading this one.",
"prismaRelationFieldName": "nextPost"
},
{
"name": "publicationId",
"type": "Publication?",
"description": "Posts are bundled in publications.",
"prismaRelationFieldName": "posts"
// cloningStrategy: 'withParent',
}
],
"examples": [
{
"id": "Post 1",
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus.",
"authorId": "User 1",
"nextPostId": "Post 2",
"publicationId": "Publication 1"
},
{
"id": "Post 2",
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"authorId": "User 1",
"publicationId": "Publication 1"
}
]
},
{
"name": "Publication",
"description": "A collection of posts.",
"fields": [
{
"name": "featuredPostId",
"type": "Post?",
"description": "The post that is featured in the publication.",
"prismaRelationFieldName": "featuredPost"
},
{
"name": "mostLikedPostId",
"type": "Post?",
"description": "The post that is most liked in the publication.",
"prismaRelationFieldName": "mostLikedPost"
}
]
}
]
}
- Extend validations:
- Validate seed, example and systemUser structure
- Verify/extend Prisma relations:
Currently, we only store the databaseName (e.g. "nextPostId") and prismaRelationFieldName` (e.g. ("nextPost")). However, for full schema generation, we probably also need the back reference (e.g. "previousPosts").
- Extend schema:
- Add cloning strategy: withParent
- Generators config