@minatojs/tests
Version:
Test Cases for Minato
649 lines (576 loc) • 21.6 kB
text/typescript
import { mapValues, isNullable, deduplicate, omit } from 'cosmokit'
import { $, Database, Field, getCell, Type, unravel } from 'minato'
import { expect } from 'chai'
interface DType {
id: number
text?: string
num?: number
double?: number
decimal?: number
int64?: bigint
bool?: boolean
list?: string[]
array?: number[]
object?: {
text?: string
num?: number
json?: {
text?: string
num?: number
},
embed?: {
bool?: boolean
bigint?: bigint
int64?: bigint
custom?: Custom
bstr?: string
}
}
object2?: {
text?: string
num?: number
embed?: {
bool?: boolean
bigint?: bigint
}
}
timestamp?: Date
date?: Date
time?: Date
binary?: ArrayBuffer
bigint?: bigint
bnum?: number
bnum2?: number
}
interface DObject {
id: number
foo?: {
nested: DType
}
bar?: {
nested: DType
}
baz?: {
nested?: DType
}[]
}
interface Custom {
a: string
b: number
}
interface RecursiveX {
id: number
y?: RecursiveY
}
interface RecursiveY {
id: number
x?: RecursiveX
}
interface Tables {
dtypes: DType
dobjects: DObject
recurxs: RecursiveX
}
interface Types {
bigint2: bigint
custom: Custom
recurx: RecursiveX
recury: RecursiveY
}
function toBinary(source: string): ArrayBuffer {
return new TextEncoder().encode(source).buffer
}
function flatten(type: any, prefix) {
if (typeof type === 'object' && type?.type === 'object') {
const result = {}
for (const key in type.inner) {
Object.assign(result, flatten(type.inner[key]!, `${prefix}.${key}`))
}
return result
} else {
return { [prefix]: type } as any
}
}
function ModelOperations(database: Database<Tables, Types>) {
database.define('bigint2', {
type: 'string',
dump: value => isNullable(value) ? value : value.toString(),
load: value => isNullable(value) ? value : BigInt(value),
initial: 123n,
})
database.define('custom', {
type: 'string',
dump: value => isNullable(value) ? value : `${value.a}|${value.b}`,
load: value => isNullable(value) ? value : { a: value.split('|')[0], b: +value.split('|')[1] },
})
const bnum = database.define({
type: 'binary',
dump: value => isNullable(value) ? value : toBinary(String(value)),
load: value => isNullable(value) ? value : +Buffer.from(value),
initial: 0,
})
const bstr = database.define({
type: 'custom',
dump: value => isNullable(value) ? value : { a: value, b: 1 },
load: value => isNullable(value) ? value : value.a,
initial: 'pooo',
})
database.define('recurx', {
type: 'object',
inner: {
id: 'unsigned',
y: 'recury',
},
})
database.define('recury', {
type: 'object',
inner: {
id: 'unsigned',
x: 'recurx',
},
})
const baseFields: Field.Extension<DType, Types> = {
id: 'unsigned',
text: {
type: 'string',
initial: 'he`l"\'\\lo',
},
num: {
type: 'integer',
initial: 233,
},
double: {
type: 'double',
initial: 3.14,
},
decimal: {
type: 'decimal',
scale: 3,
initial: 12413,
},
int64: {
type: 'bigint',
initial: 1n,
},
bool: {
type: 'boolean',
initial: true,
},
list: {
type: 'list',
initial: ['a`a', 'b"b', 'c\'c', 'd\\d'],
},
array: 'array',
object: {
type: 'object',
inner: {
num: 'unsigned',
text: 'string',
json: 'object',
embed: {
type: 'object',
inner: {
bool: {
type: 'boolean',
initial: false,
},
int64: 'bigint',
bigint: 'bigint2',
custom: { type: 'custom' },
bstr: bstr,
},
},
},
},
// dot defined object
'object2.num': {
type: 'unsigned',
initial: 1,
},
'object2.text': {
type: 'string',
initial: '2',
},
'object2.embed.bool': {
type: 'boolean',
initial: true,
},
'object2.embed.bigint': 'bigint2',
timestamp: {
type: 'timestamp',
initial: new Date('1970-01-01 00:00:00'),
},
date: {
type: 'date',
initial: new Date('1970-01-01'),
},
time: {
type: 'time',
initial: new Date('1970-01-01 12:00:00'),
},
binary: {
type: 'binary',
initial: toBinary('initial buffer')
},
bigint: 'bigint2',
bnum,
bnum2: {
type: 'binary',
dump: value => isNullable(value) ? value : toBinary(String(value)),
load: value => isNullable(value) ? value : +Buffer.from(value),
initial: 0,
},
}
const baseObject = {
type: 'object',
inner: { nested: { type: 'object', inner: baseFields } },
initial: { nested: { id: 1 } }
}
database.extend('dtypes', {
...baseFields
}, { autoInc: true })
database.extend('dobjects', {
id: 'unsigned',
foo: baseObject,
...flatten(baseObject, 'bar'),
baz: {
type: 'array',
inner: baseObject,
initial: []
},
}, { autoInc: true })
database.extend('recurxs', {
id: 'unsigned',
y: 'recury',
}, { autoInc: true })
}
namespace ModelOperations {
const magicBorn = new Date('1970/08/17')
const dtypeTable: DType[] = [
{ id: 1, bool: false },
{ id: 2, text: 'pku' },
{ id: 3, num: 1989 },
{ id: 4, list: ['1', '1', '4'], array: [1, 1, 4] },
{ id: 5, object: { num: 10, text: 'ab', embed: { bool: true, bigint: 90n, int64: 100n, bstr: 'world' } } },
{ id: 6, object2: { num: 10, text: 'ab', embed: { bool: false, bigint: 90n } } },
{ id: 7, timestamp: magicBorn },
{ id: 8, date: magicBorn },
{ id: 9, time: new Date('1999-10-01 15:40:00') },
{ id: 10, binary: toBinary('hello') },
{ id: 11, bigint: BigInt(1e63) },
{ id: 12, decimal: 2.432, int64: 9223372036854775806n },
{ id: 13, bnum: 114514, bnum2: 12345 },
{ id: 14, object: { embed: { custom: { a: 'abc', b: 123 } } } },
]
const dobjectTable: DObject[] = [
{ id: 1 },
{ id: 2, foo: { nested: { id: 1, int64: 123n, list: ['1', '1', '4'], array: [1, 1, 4], object: { num: 10, text: 'ab', embed: { bool: false, bigint: BigInt(1e163), custom: { a: '?', b: 8 }, bstr: 'wo' } }, bigint: BigInt(1e63), bnum: 114514, bnum2: 12345 } } },
{ id: 3, bar: { nested: { id: 1, list: ['1', '1', '4'], array: [1, 1, 4], object: { num: 10, text: 'ab', embed: { bool: false, bigint: BigInt(1e163), custom: { a: '?', b: 8 }, bstr: 'wo' } }, bigint: BigInt(1e63), bnum: 114514, bnum2: 12345 } } },
{ id: 4, baz: [{ nested: { id: 1, list: ['1', '1', '4'], array: [1, 1, 4], object: { num: 10, text: 'ab', embed: { bool: false, bigint: BigInt(1e163), custom: { a: '?', b: 8 }, bstr: 'wo' } }, bigint: BigInt(1e63), bnum: 114514, bnum2: 12345 } }, { nested: { id: 2 } }] },
{ id: 5, foo: { nested: { id: 1, list: ['1', '1', '4'], array: [1, 1, 4], object2: { num: 10, text: 'ab', embed: { bool: false, bigint: BigInt(1e163) } }, bigint: BigInt(1e63), bnum: 114514, bnum2: 12345 } } },
{ id: 6, bar: { nested: { id: 1, list: ['1', '1', '4'], array: [1, 1, 4], object2: { num: 10, text: 'ab', embed: { bool: false, bigint: BigInt(1e163) } }, bigint: BigInt(1e63), bnum: 114514, bnum2: 12345 } } },
{ id: 7, baz: [{ nested: { id: 1, list: ['1', '1', '4'], array: [1, 1, 4], object2: { num: 10, text: 'ab', embed: { bool: false, bigint: BigInt(1e163) } }, bigint: BigInt(1e63), bnum: 114514, bnum2: 12345 } }, { nested: { id: 2 } }] },
]
async function setup<K extends keyof Tables>(database: Database<Tables>, name: K, table: Tables[K][]) {
await database.remove(name, {})
const result: Tables[K][] = []
for (const item of table) {
result.push(await database.create(name, item as any))
}
return result
}
interface ModelOptions {
cast?: boolean
typeModel?: boolean
aggregateNull?: boolean
nullableComparator?: boolean
}
export const fields = function Fields(database: Database<Tables, Types>, options: ModelOptions = {}) {
const { cast = true, typeModel = true } = options
it('basic', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
table.forEach((row, i) => expect(row).to.have.shape(omit(dtypeTable[i], ['date', 'time'])))
await expect(database.get('dtypes', {})).to.eventually.have.deep.members(table)
await database.remove('dtypes', {})
await database.upsert('dtypes', dtypeTable)
await expect(database.get('dtypes', {})).to.eventually.have.deep.members(table)
})
typeModel && it('pass view to binary', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
table[0].binary = toBinary('this is Buffer')
await database.set('dtypes', table[0].id, { binary: Buffer.from('this is Buffer') })
await expect(database.get('dtypes', {})).to.eventually.have.deep.members(table)
})
it('modifier', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
await database.remove('dtypes', {})
await database.upsert('dtypes', dtypeTable.map(({ id }) => ({ id })))
await Promise.all(table.map(({ id, ...x }) => database.set('dtypes', id, x)))
await expect(database.get('dtypes', {})).to.eventually.have.deep.members(table)
})
it('dot notation in modifier', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
table[0].object = {}
await database.set('dtypes', table[0].id, row => ({
object: {}
}))
await expect(database.get('dtypes', table[0].id)).to.eventually.deep.eq([table[0]])
table[0].object = {
num: 123,
json: {
num: 456,
},
embed: {
bool: true,
bigint: 123n,
custom: {
a: 'a',
b: 1,
}
}
}
await database.set('dtypes', table[0].id, row => ({
'object.num': 123,
'object.json.num': 456,
'object.embed.bool': true,
'object.embed.bigint': 123n,
'object.embed.custom': { a: 'a', b: 1 },
}))
await expect(database.get('dtypes', table[0].id)).to.eventually.deep.eq([table[0]])
})
it('using expressions in modifier', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
table[0].object!.json!.num! = 543 + (table[0].object!.json!.num ?? 0)
table[0].object!.embed!.bool! = !table[0].object!.embed!.bool!
table[0].object!.embed!.bigint = 999n
await database.set('dtypes', table[0].id, row => ({
'object.json.num': $.add($.ifNull(row.object.json.num, 0), 543),
'object.embed.bool': $.not(row.object.embed.bool),
'object.embed.bigint': 999n,
}))
await expect(database.get('dtypes', {})).to.eventually.have.deep.members(table)
table[0].object!.embed!.bool! = false
await database.set('dtypes', table[0].id, {
'object.embed.bool': false,
})
await expect(database.get('dtypes', {})).to.eventually.have.deep.members(table)
table[0].object!.embed!.bool! = true
await database.set('dtypes', table[0].id, {
'object.embed.bool': true,
})
await expect(database.get('dtypes', {})).to.eventually.have.deep.members(table)
})
it('primitive', async () => {
expect(Type.fromTerm($.literal(123)).type).to.equal(Type.Number.type)
expect(Type.fromTerm($.literal('abc')).type).to.equal(Type.String.type)
expect(Type.fromTerm($.literal(true)).type).to.equal(Type.Boolean.type)
expect(Type.fromTerm($.literal(new Date('1970-01-01'))).type).to.equal('timestamp')
expect(Type.fromTerm($.literal(toBinary('hello'))).type).to.equal('binary')
expect(Type.fromTerm($.literal([1, 2, 3])).type).to.equal('json')
expect(Type.fromTerm($.literal({ a: 1 })).type).to.equal('json')
})
cast && it('cast newtype', async () => {
await setup(database, 'dtypes', dtypeTable)
await expect(database.get('dtypes', row => $.eq(row.bigint as any, $.literal(234n, 'bigint2')))).to.eventually.have.length(0)
await expect(database.get('dtypes', row => $.eq(row.bigint as any, $.literal(BigInt(1e63), 'bigint2')))).to.eventually.have.length(1)
})
typeModel && it('$.object encoding', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
await expect(database.eval('dtypes', row => $.array($.object(row)))).to.eventually.have.deep.members(table)
})
typeModel && it('$.object decoding', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
await expect(database.select('dtypes')
.project({
obj: row => $.object(row)
})
.project(mapValues(database.tables['dtypes'].fields as any, (field, key) => row => row.obj[key]))
.execute()
).to.eventually.have.deep.members(table)
})
typeModel && it('$.array encoding on cell', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
await expect(database.eval('dtypes', row => $.array(row.object))).to.eventually.have.deep.members(table.map(x => x.object))
await expect(database.eval('dtypes', row => $.array(row.object2))).to.eventually.have.deep.members(table.map(x => x.object2))
})
it('$.array encoding', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
await Promise.all(Object.keys(database.tables['dtypes'].fields).map(
key => expect(database.eval('dtypes', row => $.array(row[key]))).to.eventually.have.deep.members(table.map(x => getCell(x, key)))
))
})
it('subquery encoding', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
await Promise.all(Object.keys(database.tables['dtypes'].fields).map(
key => expect(database.select('dtypes', 1)
.project({
x: row => database.select('dtypes').evaluate(key as any)
})
.execute()
).to.eventually.have.shape([{ x: table.map(x => getCell(x, key)) }])
))
})
it('object query', async () => {
const table = await setup(database, 'dtypes', dtypeTable)
await expect(database.get('dtypes', {
'object.embed.bool': true,
})).to.eventually.have.shape([table[4]])
await expect(database.get('dtypes', {
'object.num': {
$gte: 10,
},
})).to.eventually.have.shape([table[4]])
table[4].object!.embed!.bool = false
await expect(database.set('dtypes', {
object: {
embed: {
int64: 100n,
},
},
}, {
'object.embed.bool': false,
})).to.eventually.fulfilled
await expect(database.get('dtypes', {
object: {
embed: {
int64: 100n,
},
},
})).to.eventually.deep.equal([table[4]])
await expect(database.get('dtypes', {
$or: [
{
object: {
num: 10,
},
},
{
object2: {
num: {
$gte: 10,
},
},
},
],
})).to.eventually.have.deep.members([table[4], table[5]])
})
it('recursive type', async () => {
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)
})
}
export const object = function ObjectFields(database: Database<Tables, Types>, options: ModelOptions = {}) {
const { aggregateNull = true, nullableComparator = true, typeModel = true } = options
it('basic', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
await expect(database.get('dobjects', {})).to.eventually.have.deep.members(table)
await database.remove('dobjects', {})
await database.upsert('dobjects', dobjectTable)
await expect(database.get('dobjects', {})).to.eventually.have.deep.members(table)
})
it('modifier', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
await database.remove('dobjects', {})
await database.upsert('dobjects', dobjectTable.map(({ id }) => ({ id })))
await Promise.all(table.map(({ id, ...x }) => database.set('dobjects', id, x)))
await expect(database.get('dobjects', {})).to.eventually.have.deep.members(table)
})
it('dot notation in modifier', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
table[0].foo!.nested = { id: 1 }
await database.set('dobjects', table[0].id, row => ({
'foo.nested': { id: 1 }
}))
await expect(database.get('dobjects', table[0].id)).to.eventually.deep.eq([table[0]])
table[0].foo!.nested = {
id: 1,
timestamp: new Date('2009/10/01 15:40:00'),
date: new Date('1999/10/01'),
binary: toBinary('boom'),
}
table[0].bar!.nested = {
...table[0].bar?.nested,
id: 9,
timestamp: new Date('2009/10/01 15:40:00'),
date: new Date('1999/10/01'),
binary: toBinary('boom'),
}
await database.set('dobjects', table[0].id, {
'foo.nested.timestamp': new Date('2009/10/01 15:40:00'),
'foo.nested.date': new Date('1999/10/01'),
'foo.nested.binary': toBinary('boom'),
'bar.nested.id': 9,
'bar.nested.timestamp': new Date('2009/10/01 15:40:00'),
'bar.nested.date': new Date('1999/10/01'),
'bar.nested.binary': toBinary('boom'),
})
await expect(database.get('dobjects', table[0].id)).to.eventually.deep.eq([table[0]])
table[0].baz = [{}, {}]
await database.set('dobjects', table[0].id, {
baz: [{}, {}]
})
await expect(database.get('dobjects', table[0].id)).to.eventually.deep.eq([table[0]])
})
typeModel && it('$.object encoding', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
await expect(database.eval('dobjects', row => $.array($.object(row)))).to.eventually.have.deep.members(table)
})
typeModel && it('$.object decoding', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
await expect(database.select('dobjects')
.project({
obj: row => $.object(row)
})
.project(mapValues(database.tables['dobjects'].fields as any, (field, key) => row => row.obj[key]))
.execute()
).to.eventually.have.deep.members(table)
})
aggregateNull && it('$.array encoding', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
await Promise.all(Object.keys(database.tables['dobjects'].fields).map(
key => expect(database.eval('dobjects', row => $.array(row[key]))).to.eventually.have.deep.members(table.map(x => getCell(x, key)))
))
})
it('$.array encoding boxed', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
await Promise.all(Object.keys(database.tables['dobjects'].fields).map(
key => expect(database.eval('dobjects', row => $.array($.object({ x: row[key] })))).to.eventually.have.deep.members(table.map(x => ({ x: getCell(x, key) })))
))
})
it('subquery encoding', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
await Promise.all(['baz'].map(
key => expect(database.select('dobjects', 1)
.project({
x: row => database.select('dobjects').evaluate(key as any)
})
.execute()
).to.eventually.have.shape([{ x: table.map(x => getCell(x, key)) }])
))
})
it('project with dot notation', async () => {
const table = await setup(database, 'dobjects', dobjectTable)
const keys = deduplicate([
'foo.nested.object',
'foo.nested.object.embed',
...Object.keys(database.tables['dobjects'].fields).flatMap(k => k.split('.').reduce((arr, c) => arr.length ? [`${arr[0]}.${c}`, ...arr] : [c], [])),
])
await Promise.all(keys.map(key =>
expect(database.select('dobjects').project([key as any]).execute()).to.eventually.have.deep.members(table.map(row => unravel({ [key]: getCell(row, key) })))
))
})
it('bitwise ops on bigint', async () => {
await setup(database, 'dobjects', dobjectTable)
await expect(database.get('dobjects', row => $.eq($.and(row.foo!.nested!.int64!, 5n), 1n))).to.eventually.have.length(1)
await expect(database.get('dobjects', row => $.eq($.or(row.foo!.nested!.int64!, 4n), 127n))).to.eventually.have.length(1)
await expect(database.get('dobjects', row => $.eq($.xor(row.foo!.nested!.int64!, 2n), 121n))).to.eventually.have.length(1)
await expect(database.eval('dobjects', row => $.max($.or(row.foo!.nested!.int64!, 9223372036854775701n)))).eventually.to.deep.equal(9223372036854775807n)
})
nullableComparator && it('nested $get', async () => {
await setup(database, 'dobjects', dobjectTable)
await expect(database.get('dobjects', row => $.eq(row.baz[0].nested.id, 1))).to.eventually.have.length(2)
await expect(database.get('dobjects', row => $.eq(row.baz[0].nested.array[0], 1))).to.eventually.have.length(2)
})
}
}
export default ModelOperations