Skip to content

Commit

Permalink
fix(mongo): mis-order of eval and dump, $.literal (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hieuzest authored Nov 6, 2024
1 parent 5d08c90 commit 0dc9e37
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 22 deletions.
5 changes: 1 addition & 4 deletions packages/core/src/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,7 @@ Eval.xor = multary('xor', (args, data) => {
}, (...args) => Type.fromTerms(args, Type.Boolean))

// typecast
Eval.literal = multary('literal', ([value, type]) => {
if (type) throw new TypeError('literal cast is not supported')
else return value
}, (value, type) => type ? Type.fromField(type) : Type.fromTerm(value))
Eval.literal = multary('literal', ([value, type]) => value, (value, type) => type ? Type.fromField(type) : Type.fromTerm(value))
Eval.number = unary('number', (arg, data) => {
const value = executeEval(data, arg)
return value instanceof Date ? Math.floor(value.valueOf() / 1000) : Number(value)
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clone, deepEqual, defineProperty, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit'
import { Context } from 'cordis'
import { Eval, Update } from './eval.ts'
import { Eval, isEvalExpr, Update } from './eval.ts'
import { DeepPartial, FlatKeys, Flatten, isFlat, Keys, Row, unravel } from './utils.ts'
import { Type } from './type.ts'
import { Driver } from './driver.ts'
Expand Down Expand Up @@ -353,6 +353,8 @@ export class Model<S = any> {
result[key] = obj[key]
} else if (type.type !== 'json') {
result[key] = this.resolveValue(type, obj[key])
} else if (isEvalExpr(obj[key])) {
result[key] = obj[key]
} else if (type.inner && Type.isArray(type) && Array.isArray(obj[key])) {
result[key] = obj[key].map(x => this.resolveModel(x, Type.getInner(type)))
} else if (type.inner) {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export namespace Type {
// FIXME: Type | Field<T> | Field.Type<T> | Keys<N, T> | Field.NewType<T>
export function fromField<T, N>(field: any): Type<T, N> {
if (isType(field)) return field
if (typeof field === 'string') return defineProperty({ type: field }, kType, true) as never
else if (field === 'array') return Array() as never
else if (field === 'object') return Object() as never
else if (typeof field === 'string') return defineProperty({ type: field }, kType, true) as never
else if (field.type) return field.type
else if (field.expr?.[kType]) return field.expr[kType]
throw new TypeError(`invalid field: ${field}`)
Expand Down
31 changes: 19 additions & 12 deletions packages/mongo/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ export class Builder {
$power: (arg, group) => ({ $pow: arg.map(val => this.eval(val, group)) }),
$random: (arg, group) => ({ $rand: {} }),

$literal: (arg, group) => {
return { $literal: this.dump(arg[0], arg[1] ? Type.fromField(arg[1]) : undefined) }
$literal: ([value, type], group) => {
return { $literal: this.dump(value, type ? Type.fromField(type) : undefined) }
},
$number: (arg, group) => {
const value = this.eval(arg, group)
Expand Down Expand Up @@ -628,16 +628,23 @@ export class Builder {
return type.parse(result)
}

formatUpdateAggr(model: Type, obj: any) {
const result = {}
for (const key in obj) {
const type = Type.getInner(model, key)
if (!type || type.type !== 'json' || isNullable(obj[key]) || obj[key].$literal) result[key] = obj[key]
else if (Type.isArray(type) && Array.isArray(obj[key])) result[key] = obj[key]
else if (Object.keys(obj[key]).length === 0) result[key] = { $literal: obj[key] }
else if (type.inner) result[key] = this.formatUpdateAggr(type, obj[key])
else result[key] = obj[key]
toUpdateExpr(value: any, type: Type | undefined, root: boolean = true) {
if (isNullable(value)) {
return value
} else if (isEvalExpr(value)) {
return root ? this.eval(value) : this.dump(value, type)
} else if ((type?.type === 'string' || !type) && typeof value === 'string' && value.startsWith('$')) {
return { $literal: value }
} else if ((type?.type === 'json' || !type) && typeof value === 'object' && Object.keys(value).length === 0) {
return { $literal: value }
} else if (!type) {
return this.dump(value, type)
} else if (Type.isArray(type) && Array.isArray(value)) {
return value.map(val => this.toUpdateExpr(val, type.inner, false))
} else if (type.inner) {
return mapValues(value, (val, key) => this.toUpdateExpr(val, Type.getInner(type, key), false))
} else {
return this.dump(value, type)
}
return result
}
}
6 changes: 2 additions & 4 deletions packages/mongo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,7 @@ export class MongoDriver extends Driver<MongoDriver.Config> {
const coll = this.db.collection(table)

const transformer = new Builder(this, Object.keys(sel.tables), this.getVirtualKey(table), '$' + tempKey + '.')
const $set = this.builder.formatUpdateAggr(model.getType(), mapValues(this.builder.dump(update, model),
(value: any) => typeof value === 'string' && value.startsWith('$') ? { $literal: value } : transformer.eval(value)))
const $set = mapValues(update, (item: any, key) => transformer.toUpdateExpr(item, model.getType(key)))
const $unset = Object.entries($set)
.filter(([_, value]) => typeof value === 'object')
.map(([key, _]) => key)
Expand Down Expand Up @@ -474,8 +473,7 @@ export class MongoDriver extends Driver<MongoDriver.Config> {
for (const update of data) {
const query = this.transformQuery(sel, pick(update, keys), table)!
const transformer = new Builder(this, Object.keys(sel.tables), this.getVirtualKey(table), '$' + tempKey + '.')
const $set = this.builder.formatUpdateAggr(model.getType(), mapValues(this.builder.dump(update, model),
(value: any) => typeof value === 'string' && value.startsWith('$') ? { $literal: value } : transformer.eval(value)))
const $set = mapValues(update, (item: any, key) => transformer.toUpdateExpr(item, model.getType(key)))
const $unset = Object.entries($set)
.filter(([_, value]) => typeof value === 'object')
.map(([key, _]) => key)
Expand Down
102 changes: 102 additions & 0 deletions packages/tests/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,21 @@ interface Baz {
nums: number[]
}

interface Bax {
id: number
array: {
text: string
}[]
object: {
num: number
}
}

interface Tables {
foo: Foo
bar: Bar
baz: Baz
bax: Bax
}

function JsonTests(database: Database<Tables>) {
Expand Down Expand Up @@ -62,6 +73,25 @@ function JsonTests(database: Database<Tables>) {
}
})

database.extend('bax', {
id: 'unsigned',
array: {
type: 'array',
inner: {
type: 'object',
inner: {
text: 'string',
},
},
},
object: {
type: 'object',
inner: {
num: 'unsigned',
},
},
})

before(async () => {
await setup(database, 'foo', [
{ id: 1, value: 0 },
Expand All @@ -84,6 +114,11 @@ function JsonTests(database: Database<Tables>) {
}

namespace JsonTests {
const Bax = [{
id: 1,
array: [{ text: 'foo' }],
}]

export interface RelationOptions {
nullableComparator?: boolean
}
Expand Down Expand Up @@ -176,6 +211,73 @@ namespace JsonTests {
})
}

export function modify(database: Database<Tables>) {
it('$.object', async () => {
await setup(database, 'bax', Bax)
await database.set('bax', 1, row => ({
object: $.object({
num: row.id,
}),
}))
await expect(database.get('bax', 1)).to.eventually.deep.equal([
{ id: 1, array: [{ text: 'foo' }], object: { num: 1 } },
])
})

it('$.literal', async () => {
await setup(database, 'bax', Bax)

await database.set('bax', 1, {
array: $.literal([{ text: 'foo2' }]),
})
await expect(database.get('bax', 1)).to.eventually.deep.equal([
{ id: 1, array: [{ text: 'foo2' }], object: { num: 0 } },
])

await database.set('bax', 1, {
object: $.literal({ num: 2 }),
})
await expect(database.get('bax', 1)).to.eventually.deep.equal([
{ id: 1, array: [{ text: 'foo2' }], object: { num: 2 } },
])

await database.set('bax', 1, {
'object.num': $.literal(3),
})
await expect(database.get('bax', 1)).to.eventually.deep.equal([
{ id: 1, array: [{ text: 'foo2' }], object: { num: 3 } },
])
})

it('$.literal cast', async () => {
await setup(database, 'bax', Bax)

await database.set('bax', 1, {
array: $.literal([{ text: 'foo2' }], 'array'),
})
await expect(database.get('bax', 1)).to.eventually.deep.equal([
{ id: 1, array: [{ text: 'foo2' }], object: { num: 0 } },
])

await database.set('bax', 1, {
object: $.literal({ num: 2 }, 'object'),
})
await expect(database.get('bax', 1)).to.eventually.deep.equal([
{ id: 1, array: [{ text: 'foo2' }], object: { num: 2 } },
])
})

it('nested illegal string', async () => {
await setup(database, 'bax', Bax)
await database.set('bax', 1, row => ({
array: [{ text: '$foo2' }],
}))
await expect(database.get('bax', 1)).to.eventually.deep.equal([
{ id: 1, array: [{ text: '$foo2' }], object: { num: 0 } },
])
})
}

export function selection(database: Database<Tables>) {
it('$.object', async () => {
const res = await database.select('foo')
Expand Down
17 changes: 17 additions & 0 deletions packages/tests/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface DType {
bigint?: bigint
bnum?: number
bnum2?: number
text2?: string
}

interface DObject {
Expand Down Expand Up @@ -83,6 +84,7 @@ interface Types {
custom: Custom
recurx: RecursiveX
recury: RecursiveY
string2: string
}

function toBinary(source: string): ArrayBuffer {
Expand Down Expand Up @@ -129,6 +131,13 @@ function ModelOperations(database: Database<Tables, Types>) {
initial: 'pooo',
})

database.define('string2', {
type: 'string',
dump: value => isNullable(value) ? value : `AAA${value}`,
load: value => isNullable(value) ? value : value.slice(3),
initial: '',
})

database.define('recurx', {
type: 'object',
inner: {
Expand Down Expand Up @@ -236,6 +245,7 @@ function ModelOperations(database: Database<Tables, Types>) {
load: value => isNullable(value) ? value : +Buffer.from(value),
initial: 0,
},
text2: 'string2',
}

const baseObject = {
Expand Down Expand Up @@ -511,6 +521,13 @@ namespace ModelOperations {
const table = await setup(database, 'recurxs', [{ id: 1, y: { id: 2, x: { id: 3, y: { id: 4, x: { id: 5 } } } } }])
await expect(database.get('recurxs', {})).to.eventually.have.deep.members(table)
})

it('customized string type', async () => {
await setup(database, 'dtypes', dtypeTable)
await database.set('dtypes', 1, { text2: 'foo' })
await expect(database.eval('dtypes', row => $.array(row.text2))).to.eventually.contain('foo')
await expect(database.get('dtypes', row => $.eq(row.text2, $.literal('foo', 'string2')))).to.eventually.have.length(1)
})
}

export const object = function ObjectFields(database: Database<Tables, Types>, options: ModelOptions = {}) {
Expand Down

0 comments on commit 0dc9e37

Please sign in to comment.