vine icon indicating copy to clipboard operation
vine copied to clipboard

Discussion for a new feature - Partial object

Open woubuc opened this issue 1 year ago • 9 comments

Basically the Partial<T> modifier in Typescript.

I use PATCH for a lot of update routes, where the client can send only the field(s) that changed instead of sending the entire object each time. So instead of doing this:

vine.object({
  name:        vine.string().optional(),
  description: vine.string().optional(),
  imageUrl:    vine.string().url().optional(),
  // etc.optional()
})

I would love to be able to do this:

vine.object({
  name:        vine.string(),
  description: vine.string(),
  imageUrl:    vine.string().url(),
  // etc
}).partial()

Which makes all fields in the object optional (but not the object itself). I think this would make the schema a lot easier to read and communicates the intent better.

A downside I can see is that it may be more difficult to pick up at a glance that this is a partial object - especially if it's a complex object with lots of nested properties so the .partial() modifier is pushed way down. But the inferred type would still make this obvious right away when using it so I don't think that's necessarily a problem.

Would love to hear y'all's thoughts on this.

woubuc avatar Nov 08 '24 13:11 woubuc

Will it make all the nested properties as optional too?

thetutlage avatar Nov 13 '24 07:11 thetutlage

I'd say no, only the properties of the object itself would be made optional.

There may be room for a 'deep partial' variant but I don't see as many uses for that one personally.

woubuc avatar Nov 13 '24 16:11 woubuc

+1 my current workaround is (since i dont like re-typing same fields for POST body) to map all fields from createUser with .optional() and removing the password field.

const createUserSchema = vine.object({
  email: vine.string().optional(),
  userName: vine.string().optional(),
  displayName: vine.string(),
  password: vine.string().optional(),
  type: vine.enum(UserType),
})
const { password, ...omitPassUser } = createUserSchema.getProperties()
const optionalOmitPassUser = Object.fromEntries(
  Object.entries(omitPassUser).map(([key, value]) => [key, value.optional()]),
)
const updateUserSchema = vine.object({
  ...optionalOmitPassUser,
  params: vine.object({ id: vine.string() }),
})

this works for me but i admit it's very ugly. While at it, is there a more elegant or proper way to omit a certain field from an object?

KeentGG avatar Dec 26 '24 18:12 KeentGG

A downside I can see is that it may be more difficult to pick up at a glance that this is a partial object - especially if it's a complex object with lots of nested properties so the .partial() modifier is pushed way down. But the inferred type would still make this obvious right away when using it so I don't think that's necessarily a problem.

Would love to hear y'all's thoughts on this.

Can do like vine.partial(vine.object({...}, {deep: false}))?

KeentGG avatar Dec 28 '24 11:12 KeentGG

+1

zakariamehbi avatar Jan 14 '25 10:01 zakariamehbi

hey,

i hacked my way around adonisjs & vinejs to put my validation rules in the model. my function used in the parent base model below is inherited by my other models.

public static getVineSchema(): VineObject<{}, {}, {}, {}> {
    let finalVineSchema = vine.object({})
    const columnsDefinitions = Array.from(this.$columnsDefinitions.values())
    const blacklistedColumns: string[] = ['id', 'created_at', 'updated_at', 'deleted_at']

    for (const columnDefinition of Object.values(columnsDefinitions)) {
        const { columnName, meta } = columnDefinition as ModelColumnOptions

        if (blacklistedColumns.includes(columnName)) {
            continue
        }

        const columnValidations: IColumnDecorator['meta']['validations'] = meta?.validations || []
        let columnType: IColumnDecorator['meta']['type'] = meta?.type
        let columnChildrenType: IColumnDecorator['meta']['children_type'] = meta?.children_type

        if (!meta || !meta?.type) {
            throw new GenericException({
                message: `Object meta is missing or incorrect for column ${column?.name}`,
                status: 400,
                code: 'base/invalid-payload',
            })
        }

        try {
            let shouldEscape = true
            let columnVineSchema: any

            if (columnType === 'array') {
                if (!columnChildrenType) {
                    throw new Error('Subtype is required for array type')
                }

                // @ts-ignore
                columnVineSchema = vine.array(vine[columnChildrenType]({})).compact()
            } else if (columnType === 'object') {
                columnVineSchema = vine.object({}).allowUnknownProperties()
            } else {
                // @ts-ignore
                columnVineSchema = vine[columnType]()
            }

            if (columnValidations && columnValidations.length) {
                for (const v of columnValidations) {
                    if (v?.name === 'noEscape') {
                        shouldEscape = false
                    } else if (v?.name) {
                        // @ts-ignore
                        columnVineSchema[v?.name](v?.options || {})
                    }
                }

                if (shouldEscape && typeof columnVineSchema.escape === 'function') {
                    columnVineSchema.escape()
                }

                if (shouldEscape && typeof columnVineSchema.trim === 'function') {
                    columnVineSchema.trim()
                }
            }

            finalVineSchema = vine.object({
                ...finalVineSchema.getProperties(),
                [columnName]: columnVineSchema.clone(),
            })
        } catch (error) {
            console.error(error)
            console.error('debugging', { columnDefinition, columnName, meta })
        }
    }

    return finalVineSchema
}
export interface IColumnDecorator {
  serializeAs?: null
  meta: {
    searchable: boolean
    type: 'string' | 'boolean' | 'number' | 'date' | 'enum' | 'object' | 'array'
    children_type?: 'string' | 'boolean' | 'number' | 'date' | 'enum' | 'object' | 'array'
    validations: {
      name:
        | 'email'
        | 'url'
        | 'uuid'
        | 'mobile'
        | 'trim'
        | 'normalizeEmail'
        | 'normalizeUrl'
        | 'escape'
        | 'toUpperCase'
        | 'toCamelCase'
        | 'toLowerCase'
        | 'optional'
        | 'nullable'
        | 'noEscape'
      options?: object
    }[]
  }
}
@column({
    meta: {
        searchable: true,
        type: 'string',
        validations: [{ name: 'uuid' }],
    },
})
declare app_uid: string

@column({
    meta: { searchable: true, type: 'string', validations: [{ name: 'noEscape' }] },
    } as IColumnDecorator)
declare timezone: string

usually i write small validations into a separate file and then use them in the controllers (request.validateUsing()). but when i need to check the full object i use my getVineSchema() function. for post/put requests i would just add a parameter to the getVineSchema() function partial boolean and apply optional() & nullable() to all the properties.

hope this helps. tell me if there is an easier way to do model-based validations.

zakariamehbi avatar Jan 14 '25 10:01 zakariamehbi

+1

Fabricio-191 avatar Feb 04 '25 15:02 Fabricio-191

+1

gaoxufei avatar Apr 29 '25 07:04 gaoxufei

Definitely have the same need here. I ended up going this way : https://github.com/akago-dev/vine-lucid/ (It is tailored to lucid but would be adaptable to any ORM) You might be particulary interested in vine.lucid(Kid, { partial: true }) Would be happy to improve this solution together if any interest.

pomarec avatar Jun 17 '25 16:06 pomarec