-
Notifications
You must be signed in to change notification settings - Fork 8
Schemas validation while running base44 dev
#395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import Datastore from "@seald-io/nedb"; | ||
| import type { Entity } from "@/core/resources/entity/schema.js"; | ||
| import { type EntityRecord, Validator } from "./validator.js"; | ||
|
|
||
| export class Database { | ||
| private collections: Map<string, Datastore> = new Map(); | ||
| private schemas: Map<string, Entity> = new Map(); | ||
| private validator: Validator = new Validator(); | ||
|
|
||
| load(entities: Entity[]) { | ||
| for (const entity of entities) { | ||
| this.collections.set(entity.name, new Datastore()); | ||
| this.schemas.set(entity.name, entity); | ||
| } | ||
| } | ||
|
|
||
| getCollection(name: string): Datastore | undefined { | ||
| return this.collections.get(name); | ||
| } | ||
|
|
||
| getCollectionNames(): string[] { | ||
| return Array.from(this.collections.keys()); | ||
| } | ||
|
|
||
| dropAll() { | ||
| for (const collection of this.collections.values()) { | ||
| collection.remove({}, { multi: true }); | ||
| } | ||
| this.collections.clear(); | ||
| this.schemas.clear(); | ||
| } | ||
|
|
||
| validate(entityName: string, record: EntityRecord, partial: boolean = false) { | ||
| const schema = this.schemas.get(entityName); | ||
| if (!schema) { | ||
| throw new Error(`Entity "${entityName}" not found`); | ||
| } | ||
|
|
||
| return this.validator.validate(record, schema, partial); | ||
| } | ||
|
|
||
| prepareRecord( | ||
| entityName: string, | ||
| record: EntityRecord, | ||
| partial: boolean = false, | ||
| ) { | ||
| const schema = this.schemas.get(entityName); | ||
| if (!schema) { | ||
| throw new Error(`Entity "${entityName}" not found`); | ||
| } | ||
|
|
||
| const filteredRecord = this.validator.filterFields(record, schema); | ||
| if (partial) { | ||
| return filteredRecord; | ||
| } | ||
| return this.validator.applyDefaults(filteredRecord, schema); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| import type { | ||
| Entity, | ||
| PropertyDefinition, | ||
| } from "@/core/resources/entity/schema.js"; | ||
|
|
||
| export type EntityRecord = Record<string, unknown>; | ||
|
|
||
| type ValidationError = { | ||
| error_type: "ValidationError"; | ||
| message: string; | ||
| request_id: null; | ||
| traceback: ""; | ||
| }; | ||
|
|
||
| type ValidationResponse = | ||
| | { | ||
| hasError: false; | ||
| } | ||
| | { | ||
| hasError: true; | ||
| error: ValidationError; | ||
| }; | ||
|
|
||
| // https://docs.base44.com/developers/backend/resources/entities/entity-schemas#field-types | ||
| const fieldTypes = [ | ||
| "string", | ||
| "integer", | ||
| "number", | ||
| "boolean", | ||
| "array", | ||
| "object", | ||
| ]; | ||
|
|
||
| export class Validator { | ||
| /** | ||
| * Filter out fields that are not supported by the schema | ||
| */ | ||
| filterFields(record: EntityRecord, entitySchema: Entity): EntityRecord { | ||
| const filteredRecord: EntityRecord = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| if (entitySchema.properties[key]) { | ||
| filteredRecord[key] = value; | ||
| } | ||
| } | ||
| return filteredRecord; | ||
| } | ||
|
|
||
| applyDefaults(record: EntityRecord, entitySchema: Entity): EntityRecord { | ||
| const result: EntityRecord = {}; | ||
| for (const [key, property] of Object.entries(entitySchema.properties)) { | ||
| if (property.default !== undefined) { | ||
| result[key] = property.default; | ||
| } | ||
| } | ||
| return { | ||
| ...result, | ||
| ...record, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Validates whether record is correctly follow the schema | ||
| */ | ||
| validate( | ||
| record: EntityRecord, | ||
| entitySchema: Entity, | ||
| partial: boolean = false, | ||
| ): ValidationResponse { | ||
| // Partial validation happening, when user updates existing entity. | ||
| // In this case not all data will be passed. | ||
| if (!partial) { | ||
| const requiredFieldsResponse = this.validateRequiredFields( | ||
| record, | ||
| entitySchema, | ||
| ); | ||
| if (requiredFieldsResponse.hasError) { | ||
| return requiredFieldsResponse; | ||
| } | ||
| } | ||
| const fieldTypesResponse = this.validateFieldTypes(record, entitySchema); | ||
| if (fieldTypesResponse.hasError) { | ||
| return fieldTypesResponse; | ||
| } | ||
| return { | ||
| hasError: false, | ||
| }; | ||
| } | ||
|
|
||
| private createValidationError(message: string): ValidationError { | ||
| return { | ||
| error_type: "ValidationError", | ||
| message, | ||
| request_id: null, | ||
| traceback: "", | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Validating field types according to documentation | ||
| */ | ||
| private validateFieldTypes( | ||
| record: EntityRecord, | ||
| entitySchema: Entity, | ||
| ): ValidationResponse { | ||
| for (const [key, value] of Object.entries(record)) { | ||
| const property = entitySchema.properties[key]; | ||
| const result = this.validateValue(value, property, key); | ||
| if (result.hasError) return result; | ||
| } | ||
|
|
||
| return { | ||
| hasError: false, | ||
| }; | ||
| } | ||
|
|
||
| private validateValue( | ||
| value: unknown, | ||
| property: PropertyDefinition | undefined, | ||
| fieldPath: string, | ||
| ): ValidationResponse { | ||
| // Silently ignore fields not defined in the schema. | ||
| // The expectation is that `filterFields()` will run before. | ||
| if (!property) { | ||
| return { hasError: false }; | ||
| } | ||
|
|
||
| const propertyType = property.type; | ||
| if (!fieldTypes.includes(propertyType)) { | ||
| return { | ||
| hasError: true, | ||
| error: this.createValidationError( | ||
| `Error in field ${fieldPath}: Unsupported field type ${propertyType}`, | ||
| ), | ||
| }; | ||
| } | ||
|
|
||
| switch (propertyType) { | ||
| case "array": | ||
| if (!Array.isArray(value)) { | ||
| return { | ||
| hasError: true, | ||
| error: this.createValidationError( | ||
| `Error in field ${fieldPath}: Input should be a valid array`, | ||
| ), | ||
| }; | ||
| } | ||
| if (property.items) { | ||
| for (let i = 0; i < value.length; i++) { | ||
| const itemResult = this.validateValue( | ||
| value[i], | ||
| property.items, | ||
| `${fieldPath}[${i}]`, | ||
| ); | ||
| if (itemResult.hasError) return itemResult; | ||
| } | ||
| } | ||
| break; | ||
| case "object": | ||
| if ( | ||
| typeof value !== "object" || | ||
| value === null || | ||
| Array.isArray(value) | ||
| ) { | ||
| return { | ||
| hasError: true, | ||
| error: this.createValidationError( | ||
| `Error in field ${fieldPath}: Input should be a valid object`, | ||
| ), | ||
| }; | ||
| } | ||
| if (property.properties) { | ||
| for (const [subKey, subValue] of Object.entries( | ||
| value as Record<string, unknown>, | ||
| )) { | ||
| if (property.properties[subKey]) { | ||
| const subResult = this.validateValue( | ||
| subValue, | ||
| property.properties[subKey], | ||
| `${fieldPath}.${subKey}`, | ||
| ); | ||
| if (subResult.hasError) return subResult; | ||
| } | ||
| } | ||
| } | ||
| break; | ||
| case "integer": | ||
| if (!Number.isInteger(value)) { | ||
| return { | ||
| hasError: true, | ||
| error: this.createValidationError( | ||
| `Error in field ${fieldPath}: Input should be a valid integer`, | ||
| ), | ||
| }; | ||
| } | ||
| break; | ||
| default: | ||
| if (typeof value !== propertyType) { | ||
| return { | ||
| hasError: true, | ||
| error: this.createValidationError( | ||
| `Error in field ${fieldPath}: Input should be a valid ${propertyType}`, | ||
| ), | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| return { hasError: false }; | ||
| } | ||
|
|
||
| private validateRequiredFields( | ||
| record: EntityRecord, | ||
| entitySchema: Entity, | ||
| ): ValidationResponse { | ||
| if (entitySchema.required && entitySchema.required.length > 0) { | ||
| for (const required of entitySchema.required) { | ||
| if (record[required] == null) { | ||
| return { | ||
| hasError: true, | ||
| error: this.createValidationError( | ||
| `Error in field ${required}: Field required`, | ||
| ), | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| hasError: false, | ||
| }; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a high level idea, haven't dived into it -
But as far as i understand the entities schema are basically json-schema standard, and i saw this package @cfworker/json-schema (https://github.com/cfworker/cfworker) which we can feed the schema to and then just call validate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is that leaders don't really want to validate - this schema is just documentation of intent
And looks we're going with this approach forward.
json-schemasupports far more features than what we need to handle.Therefore not sure if need to introduce anything that will actually use this schema to validate any further than just types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looked into it once more.
It's possible to use
json-schema, not sure if makes code simpler though.I will still need to do recursions in order to make
json-schemawork with limited set of validations.And also I will need to filter out errors that are not relevant to our case.
If you want to take a look, here is a prompt that I used to test it out:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should proceed with the current implentation considering the links you added, I just don't love maintaining this validation logic (and issues that may raise) on the CLI side.. but it looks kinda simple for now 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we will need anyway maintain validation logic in some capacity 😞