Skip to content

Commit

Permalink
feat: json field type
Browse files Browse the repository at this point in the history
  • Loading branch information
mle-moni committed Oct 3, 2024
1 parent 5fb423d commit 704c4c4
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 33 deletions.
60 changes: 36 additions & 24 deletions app/adomin/create_model_view_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type {
AdominRouteOverrides,
AdominStaticRightsConfig,
} from './routes/adomin_routes_overrides_and_rights.js'
import type { AttachmentContract } from './routes/handle_files.js'
import type { AdominValidation } from './validation/adomin_validation_helpers.js'

export interface ColumnConfig {
Expand Down Expand Up @@ -126,31 +125,44 @@ export interface VirtualColumnConfig<T extends LucidModel> {
setter?: (model: InstanceType<T>, value: any) => Promise<void>
}

type LucidRelation =
| BelongsTo<LucidModel>
| HasOne<LucidModel>
| HasMany<LucidModel>
| ManyToMany<LucidModel>

type ComputeLucidRelationType<T extends LucidRelation> =
T extends BelongsTo<LucidModel>
? AdominBelongsToRelationFieldConfig
: T extends HasOne<LucidModel>
? AdominHasOneRelationFieldConfig
: T extends HasMany<LucidModel>
? AdominHasManyRelationFieldConfig
: T extends ManyToMany<LucidModel>
? AdominManyToManyRelationFieldConfig
: never

type AdominFieldTypeForNumber = AdominNumberFieldConfig | AdominForeignKeyFieldConfig

type AdominFieldTypeForString =
| AdominStringFieldConfig
| AdominEnumFieldConfig
| AdominForeignKeyFieldConfig
| AdominFileFieldConfig

type GetAdominTypeFromModelFieldType<T> = T extends number
? AdominNumberFieldConfig | AdominForeignKeyFieldConfig
? AdominFieldTypeForNumber
: T extends string
?
| AdominStringFieldConfig
| AdominEnumFieldConfig
| AdominForeignKeyFieldConfig
| AdominFileFieldConfig
: T extends BelongsTo<LucidModel>
? AdominBelongsToRelationFieldConfig
: T extends HasOne<LucidModel>
? AdominHasOneRelationFieldConfig
: T extends HasMany<LucidModel>
? AdominHasManyRelationFieldConfig
: T extends ManyToMany<LucidModel>
? AdominManyToManyRelationFieldConfig
: T extends DateTime
? AdominDateFieldConfig
: T extends boolean
? AdominBooleanFieldConfig
: T extends Array<any>
? AdominArrayFieldConfig
: T extends AttachmentContract
? AdominFileFieldConfig
: AdominFieldConfig
? AdominFieldTypeForString
: T extends LucidRelation
? ComputeLucidRelationType<T>
: T extends DateTime
? AdominDateFieldConfig
: T extends boolean
? AdominBooleanFieldConfig
: T extends Array<any>
? AdominArrayFieldConfig
: AdominFieldConfig

interface ModelConfigDynamicOptions<T extends LucidModel> {
columns: Partial<{
Expand Down
14 changes: 8 additions & 6 deletions app/adomin/fields.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MultipartFile } from '@adonisjs/core/bodyparser'
import { LucidRow } from '@adonisjs/lucid/types/model'
import { RawQuery } from '@adonisjs/lucid/types/querybuilder'
import { RawQueryBindings } from '@adonisjs/lucid/types/querybuilder'
import { VineObject, VineValidator } from '@vinejs/vine'

export interface AdominBaseFieldConfig {
/**
Expand Down Expand Up @@ -52,7 +53,7 @@ export interface AdominBaseFieldConfig {
* }
* ```
*/
sqlFilter?: (input: string | null) => string | RawQuery
sqlFilter?: (input: string | null) => string | { sql: string; bindings: RawQueryBindings }
/**
* Sql orderBy override, usefull for computed fields
*
Expand All @@ -63,7 +64,7 @@ export interface AdominBaseFieldConfig {
* }
* ```
*/
sqlSort?: (ascDesc: 'asc' | 'desc') => string | RawQuery
sqlSort?: (ascDesc: 'asc' | 'desc') => string
/**
* Export data transformation callback to use for this field
*
Expand Down Expand Up @@ -339,8 +340,9 @@ type FileSubType =
deleteFile: (model: LucidRow) => Promise<void>
}

export interface AdominObjectFieldConfig extends AdominBaseFieldConfig {
type: 'object'
export interface AdominJsonFieldConfig extends AdominBaseFieldConfig {
type: 'json'
validation?: VineValidator<VineObject<any, any, any>, any>
}

export interface AdominForeignKeyFieldConfig extends AdominBaseFieldConfig {
Expand Down Expand Up @@ -623,4 +625,4 @@ export type AdominFieldConfig =
| AdominBelongsToRelationFieldConfig
| AdominHasOneRelationFieldConfig
| AdominManyToManyRelationFieldConfig
// | AdominObjectFieldConfig
| AdominJsonFieldConfig
9 changes: 8 additions & 1 deletion app/adomin/routes/get_validation_schema_from_lucid_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ const getValidationSchemaFromFieldConfig = (
return schema.array.optional().members(schema[config.relatedKeyType ?? 'number']())
}

if (config.type === 'json') {
if (suffix) return schema.string[suffix]()

return schema.string()
}

if (config.type === 'file') {
const specialSchema = getFileSchema(validationMode, suffix)

Expand All @@ -101,7 +107,8 @@ const getType = (config: AdominFieldConfig) => {
return config.fkType ?? 'number'
case 'hasManyRelation':
case 'manyToManyRelation':
throw new Error('hasManyRelation should be handled before calling this function')
case 'json':
throw new Error(`${config.type} should be handled before calling this function`)
default:
return config.type
}
Expand Down
5 changes: 5 additions & 0 deletions app/adomin/routes/models/get_model_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ export async function computeColumnConfigFields(input: ColumnConfig[]): Promise<
if (sortable === undefined && noCustomSort) sortable = false
}

if (field.adomin.type === 'json') {
if (sortable === undefined && noCustomSort) sortable = false
if (filterable === undefined && noCustomFilter) filterable = false
}

// load options for array field
if (field.adomin.type === 'array' && typeof field.adomin.options === 'function') {
field.adomin.options = await field.adomin.options()
Expand Down
12 changes: 10 additions & 2 deletions app/adomin/routes/models/read/model_query_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ export const applyGlobalFilters = (

if (field.adomin.sqlFilter !== undefined) {
const sqlFilter = field.adomin.sqlFilter(globalFilter)
builder.andWhereRaw(sqlFilter)
if (typeof sqlFilter === 'string') {
builder.andWhereRaw(sqlFilter)
} else {
builder.andWhereRaw(sqlFilter.sql, sqlFilter.bindings)
}
continue
}

Expand Down Expand Up @@ -130,7 +134,11 @@ export const applyColumnFilters = (

if (field.adomin.sqlFilter !== undefined) {
const sqlFilter = field.adomin.sqlFilter(search)
builder.andWhereRaw(sqlFilter)
if (typeof sqlFilter === 'string') {
builder.andWhereRaw(sqlFilter)
} else {
builder.andWhereRaw(sqlFilter.sql, sqlFilter.bindings)
}
continue
}

Expand Down
3 changes: 3 additions & 0 deletions app/adomin/routes/models/write/create_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
attachForeignFields,
updateVirtualColumns,
} from './attach_fields_to_model.js'
import { handleSpecialFieldsValidation } from './handle_special_fields_validation.js'

export const createModel = async (ctx: HttpContext) => {
const { params, response, request } = ctx
Expand Down Expand Up @@ -39,6 +40,8 @@ export const createModel = async (ctx: HttpContext) => {

const schema = await getValidationSchemaFromConfig(modelConfig, 'create')
const parsedData = await request.validate({ schema, messages: getGenericMessages(Model) })
const specialFieldsValidation = await handleSpecialFieldsValidation(modelConfig, parsedData)
if (specialFieldsValidation) return response.badRequest(specialFieldsValidation)

const createdInstance = new Model()

Expand Down
43 changes: 43 additions & 0 deletions app/adomin/routes/models/write/handle_special_fields_validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ColumnConfig, ModelConfig } from '#adomin/create_model_view_config'

export type SpecialValidationResult = { error: string } | null

export const handleSpecialFieldsValidation = async (
modelConfig: ModelConfig,
parsedData: {
[x: string]: any
}
): Promise<SpecialValidationResult> => {
for (const field of modelConfig.fields) {
if (field.adomin.type === 'json') {
const res = await validateJsonField(field, parsedData)
if (res) return res
}
}

return null
}

const validateJsonField = async (
field: ColumnConfig,
parsedData: any
): Promise<SpecialValidationResult> => {
if (field.adomin.type !== 'json') return null

const parsedDataValue = parsedData[field.name]

if (!parsedDataValue) return null

try {
const jsonParsedValue = JSON.parse(parsedDataValue)
const res = await field.adomin.validation?.validate(jsonParsedValue)

parsedData[field.name] = res
} catch (error) {
return {
error: `Le champ ${field.name} (json) n'est pas valide`,
}
}

return null
}
3 changes: 3 additions & 0 deletions app/adomin/routes/models/write/update_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
attachForeignFields,
updateVirtualColumns,
} from './attach_fields_to_model.js'
import { handleSpecialFieldsValidation } from './handle_special_fields_validation.js'

export const updateModel = async (ctx: HttpContext) => {
const { params, response, request } = ctx
Expand Down Expand Up @@ -39,6 +40,8 @@ export const updateModel = async (ctx: HttpContext) => {

const schema = await getValidationSchemaFromConfig(modelConfig, 'update')
const parsedData = await request.validate({ schema, messages: getGenericMessages(Model) })
const specialFieldsValidation = await handleSpecialFieldsValidation(modelConfig, parsedData)
if (specialFieldsValidation) return response.badRequest(specialFieldsValidation)
const fields = modelConfig.fields

const modelInstance = await getModelData(Model, id)
Expand Down

0 comments on commit 704c4c4

Please sign in to comment.