From ba0a45b3b78a531f88cf9d8a60f3b27f648a219e Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Wed, 22 May 2024 23:25:44 +0800 Subject: [PATCH 01/17] feat: support reload on disconnect --- packages/tests/src/index.ts | 16 +- packages/tests/src/relation.ts | 1447 +++++++++++++++++++------------- 2 files changed, 857 insertions(+), 606 deletions(-) diff --git a/packages/tests/src/index.ts b/packages/tests/src/index.ts index c5425317..b7c79152 100644 --- a/packages/tests/src/index.ts +++ b/packages/tests/src/index.ts @@ -63,14 +63,14 @@ function createUnit(target: T, root = false): Unit { } namespace Tests { - export const model = ModelOperations - export const query = QueryOperators - export const update = UpdateOperators - export const object = ObjectOperations - export const selection = Selection - export const migration = Migration - export const json = Json - export const transaction = Transaction + // export const model = ModelOperations + // export const query = QueryOperators + // export const update = UpdateOperators + // export const object = ObjectOperations + // export const selection = Selection + // export const migration = Migration + // export const json = Json + // export const transaction = Transaction export const relation = Relation } diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index fc9d949b..7a8fbfaf 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -1,4 +1,4 @@ -import { $, Database, Query, Relation } from 'minato' +import { $, Database, Query, Relation, Values } from 'minato' import { expect } from 'chai' import { setup } from './utils' @@ -35,18 +35,37 @@ interface Tag { } interface Post2Tag { - postId: number - tagId: number post?: Post tag?: Tag } +interface GuildSync { + syncAt?: Date +} + +interface Login extends GuildSync { + id: string + platform: string + name?: string + guilds?: Guild[] +} + +interface Guild extends GuildSync { + id: string + platform2: string + name?: string + logins?: Login[] +} + interface Tables { user: User profile: Profile post: Post tag: Tag post2tag: Post2Tag + // guildSync: GuildSync + login: Login + guild: Guild } function RelationTests(database: Database) { @@ -99,22 +118,42 @@ function RelationTests(database: Database) { database.extend('post2tag', { id: 'unsigned', - postId: 'unsigned', - tagId: 'unsigned', + 'post.id': 'unsigned', + 'tag.id': 'unsigned', post: { type: 'manyToOne', table: 'post', target: '_tags', - fields: 'postId', }, tag: { type: 'manyToOne', table: 'tag', target: '_posts', - fields: 'tagId', }, }, { - primary: ['postId', 'tagId'], + primary: ['post', 'tag'], + }) + + database.extend('login', { + id: 'string', + platform: 'string', + name: 'string', + }, { + primary: ['id', 'platform'] + }) + + database.extend('guild', { + id: 'string', + platform2: 'string', + name: 'string', + logins: { + type: 'manyToMany', + table: 'login', + target: 'guilds', + shared: { platform2: 'platform' }, + }, + }, { + primary: ['id', 'platform2'] }) async function setupAutoInc(database: Database, name: K, length: number) { @@ -155,12 +194,12 @@ namespace RelationTests { ] const post2TagTable: Post2Tag[] = [ - { postId: 1, tagId: 1 }, - { postId: 1, tagId: 2 }, - { postId: 2, tagId: 1 }, - { postId: 2, tagId: 3 }, - { postId: 3, tagId: 3 }, - ] + { post: { id: 1 }, tag: { id: 1 } }, + { post: { id: 1 }, tag: { id: 2 } }, + { post: { id: 2 }, tag: { id: 1 } }, + { post: { id: 2 }, tag: { id: 3 } }, + { post: { id: 3 }, tag: { id: 3 } }, + ] as any export interface RelationOptions { ignoreNullObject?: boolean @@ -180,7 +219,7 @@ namespace RelationTests { })), ) - await expect(database.select('user', {}, { profile: true, posts: true }).execute()).to.eventually.have.shape( + await expect(database.get('user', {}, { include: { profile: true, posts: true } })).to.eventually.have.shape( users.map(user => ({ ...user, profile: profiles.find(profile => profile.id === user.id), @@ -212,7 +251,7 @@ namespace RelationTests { const profiles = await setup(database, 'profile', profileTable) const posts = await setup(database, 'post', postTable) - await expect(database.select('user', {}, { posts: { author: true } }).execute()).to.eventually.have.shape( + await expect(database.select('user', {}, { posts: { author: { successor: false } } }).execute()).to.eventually.have.shape( users.map(user => ({ ...user, posts: posts.filter(post => post.author?.id === user.id).map(post => ({ @@ -252,595 +291,807 @@ namespace RelationTests { const posts = await setup(database, 'post', postTable) const tags = await setup(database, 'tag', tagTable) const post2tags = await setup(database, 'post2tag', post2TagTable) - const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable.map(x => ({ - post_id: x.postId, - tag_id: x.tagId, - }))) + const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) // explicit manyToMany await expect(database.select('post', {}, { _tags: { tag: { _posts: { post: true } } } }).execute()).to.eventually.be.fulfilled - await expect(database.select('post', {}, { tags: { posts: true } }).execute()).to.eventually.have.shape( - posts.map(post => ({ - ...post, - tags: post2tags.filter(p2t => p2t.postId === post.id) - .map(p2t => tags.find(tag => tag.id === p2t.tagId)) - .filter(tag => tag) - .map(tag => ({ - ...tag, - posts: post2tags.filter(p2t => p2t.tagId === tag!.id).map(p2t => posts.find(post => post.id === p2t.postId)), - })), - })), - ) - }) - } - - export function query(database: Database) { - it('oneToOne / manyToOne', async () => { - const users = await setup(database, 'user', userTable) - const profiles = await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - - await expect(database.get('user', { - profile: { - user: { - id: 1, - }, - }, - })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ - ...user, - profile: profiles.find(profile => profile.id === user.id), - }))) - - await expect(database.get('user', row => $.query(row, { - profile: r => $.eq(r.id, row.id), - }))).to.eventually.have.shape(users.map(user => ({ - ...user, - profile: profiles.find(profile => profile.id === user.id), - }))) - - await expect(database.get('user', { - profile: { - user: { - value: 1, - }, - }, - })).to.eventually.have.shape(users.slice(1, 2).map(user => ({ - ...user, - profile: profiles.find(profile => profile.id === user.id), - }))) - - await expect(database.get('post', { - author: { - id: 1, - }, - })).to.eventually.have.shape(posts.map(post => ({ - ...post, - author: users.find(user => post.author?.id === user.id), - })).filter(post => post.author?.id === 1)) - - await expect(database.get('post', { - author: { - id: 1, - value: 1, - }, - })).to.eventually.have.length(0) - }) - - it('oneToMany', async () => { - const users = await setup(database, 'user', userTable) - const profiles = await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - - await expect(database.get('user', { - posts: { - $some: { - author: { - id: 1, - }, - }, - }, - })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ - ...user, - posts: posts.filter(post => post.author?.id === user.id), - }))) - - await expect(database.get('user', { - posts: { - $some: row => $.eq(row.id, 1), - }, - })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ - ...user, - posts: posts.filter(post => post.author?.id === user.id), - }))) - - await expect(database.get('user', { - posts: { - $none: { - author: { - id: 1, - }, - }, - }, - })).to.eventually.have.shape(users.slice(1).map(user => ({ - ...user, - posts: posts.filter(post => post.author?.id === user.id), - }))) - - await expect(database.get('user', { - posts: { - $every: { - author: { - id: 1, - }, - }, - }, - })).to.eventually.have.shape([users[0], users[2]].map(user => ({ - ...user, - posts: posts.filter(post => post.author?.id === user.id), - }))) - }) - - it('manyToMany', async () => { - const users = await setup(database, 'user', userTable) - const profiles = await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - const tags = await setup(database, 'tag', tagTable) - const post2tags = await setup(database, 'post2tag', post2TagTable) - const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable.map(x => ({ - post_id: x.postId, - tag_id: x.tagId, - }))) - - await expect(database.get('post', { - tags: { - $some: { - id: 1, - }, - }, - })).to.eventually.have.shape(posts.slice(0, 2).map(post => ({ - ...post, - tags: post2tags.filter(p2t => p2t.postId === post.id) - .map(p2t => tags.find(tag => tag.id === p2t.tagId)) - .filter(tag => tag), - }))) - - await expect(database.get('post', { - tags: { - $none: { - id: 1, - }, - }, - })).to.eventually.have.shape(posts.slice(2).map(post => ({ - ...post, - tags: post2tags.filter(p2t => p2t.postId === post.id) - .map(p2t => tags.find(tag => tag.id === p2t.tagId)) - .filter(tag => tag), - }))) - - await expect(database.get('post', { - tags: { - $every: { - id: 3, - }, - }, - })).to.eventually.have.shape(posts.slice(2, 3).map(post => ({ - ...post, - tags: post2tags.filter(p2t => p2t.postId === post.id) - .map(p2t => tags.find(tag => tag.id === p2t.tagId)) - .filter(tag => tag), - }))) - - await expect(database.get('post', { - tags: { - $some: 1, - $none: [3], - $every: {}, - }, - })).to.eventually.have.shape(posts.slice(0, 1).map(post => ({ - ...post, - tags: post2tags.filter(p2t => p2t.postId === post.id) - .map(p2t => tags.find(tag => tag.id === p2t.tagId)) - .filter(tag => tag), - }))) - }) - - it('nested query', async () => { - const users = await setup(database, 'user', userTable) - const profiles = await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - const tags = await setup(database, 'tag', tagTable) - const post2tags = await setup(database, 'post2tag', post2TagTable) - const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable.map(x => ({ - post_id: x.postId, - tag_id: x.tagId, - }))) - - await expect(database.get('user', { - posts: { - $some: { - tags: { - $some: { - id: 1, - }, - }, - }, - }, - })).to.eventually.have.shape([users[0]].map(user => ({ - ...user, - posts: posts.filter(post => post.author?.id === user.id), - }))) - - await expect(database.get('tag', { - posts: { - $some: { - author: { - id: 2, - }, - }, - }, - })).to.eventually.have.shape([tags[2]].map(tag => ({ - ...tag, - posts: post2tags.filter(p2t => p2t.tagId === tag.id) - .map(p2t => posts.find(post => post.id === p2t.postId)) - .filter(post => post), - }))) - }) - - it('omit query', async () => { - const users = await setup(database, 'user', userTable) - const profiles = await setup(database, 'profile', profileTable) - - await expect(database.get('user', { id: 2 }, ['id', 'profile'])).to.eventually.have.shape( - [users[1]].map(user => ({ - id: user.id, - profile: profiles.find(profile => user.id === profile.id), - })), - ) - }) - - it('existence', async () => { - await setup(database, 'user', userTable) - await setup(database, 'profile', profileTable) - await setup(database, 'post', postTable) - - await expect(database.select('user', { successor: null }, null).execute()).to.eventually.have.shape([ - { id: 1 }, - { id: 3 }, - ]) - - await expect(database.select('user', { predecessor: null }, null).execute()).to.eventually.have.shape([ - { id: 2 }, - { id: 3 }, - ]) - - await database.set('user', 1, { profile: null }) - await expect(database.select('user', { profile: null }, null).execute()).to.eventually.have.shape([ - { id: 1 }, - ]) - - await database.set('user', 2, { - posts: { - $disconnect: { - id: 3, - }, - }, - }) - await expect(database.select('post', { author: null }, null).execute()).to.eventually.have.shape([ - { id: 3 }, - ]) - await expect(database.select('user', { - posts: { - $every: { - author: null, - }, - }, - }, null).execute()).to.eventually.have.shape([ - { id: 2 }, - { id: 3 }, - ]) + // await expect(database.select('post', {}, { tags: { posts: true } }).execute()).to.eventually.have.shape( + // posts.map(post => ({ + // ...post, + // tags: post2tags.filter(p2t => p2t.post?.id === post.id) + // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + // .filter(tag => tag) + // .map(tag => ({ + // ...tag, + // posts: post2tags.filter(p2t => p2t.tag?.id === tag!.id).map(p2t => posts.find(post => post.id === p2t.post?.id)), + // })), + // })), + // ) + + console.dir((await database.select('post', {}, { tags: true }).execute()), { depth: 10}) }) } - export function create(database: Database) { - it('basic support', async () => { - await setup(database, 'user', []) - await setup(database, 'profile', []) - await setup(database, 'post', []) - - for (const user of userTable) { - await expect(database.create('user', { - ...user, - profile: { - ...profileTable.find(profile => profile.id === user.id)!, - }, - posts: postTable.filter(post => post.author?.id === user.id), - })).to.eventually.have.shape(user) - } - - await expect(database.select('profile', {}, { user: true }).execute()).to.eventually.have.shape( - profileTable.map(profile => ({ - ...profile, - user: userTable.find(user => user.id === profile.id), - })), - ) - - await expect(database.select('user', {}, { profile: true, posts: true }).execute()).to.eventually.have.shape( - userTable.map(user => ({ - ...user, - profile: profileTable.find(profile => profile.id === user.id), - posts: postTable.filter(post => post.author?.id === user.id), - })), - ) - }) - - it('oneToMany', async () => { - await setup(database, 'user', []) - await setup(database, 'profile', []) - await setup(database, 'post', []) - - for (const user of userTable) { - await database.create('user', { - ...userTable.find(u => u.id === user.id)!, - posts: postTable.filter(post => post.author?.id === user.id), - }) - } - - await expect(database.select('user', {}, { posts: true }).execute()).to.eventually.have.shape( - userTable.map(user => ({ - ...user, - posts: postTable.filter(post => post.author?.id === user.id), - })), - ) - }) - } - - export function modify(database: Database, options: RelationOptions = {}) { - const { ignoreNullObject = true } = options - - it('oneToOne / manyToOne', async () => { - const users = await setup(database, 'user', userTable) - const profiles = await setup(database, 'profile', profileTable) - await setup(database, 'post', postTable) - - profiles.splice(2, 1) - await database.set('user', 3, { - profile: null, - }) - await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) - - profiles.push(database.tables['profile'].create({ id: 3, name: 'Reborn' })) - await database.set('user', 3, { - profile: { - name: 'Reborn', - }, - }) - await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) - - users[0].value = 99 - await database.set('post', 1, { - author: { - value: 99, - }, - }) - await expect(database.get('user', {})).to.eventually.have.deep.members(users) - - profiles.splice(2, 1) - await database.set('user', 3, { - profile: null, - }) - await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) - }) - - it('upsert', async () => { - await setup(database, 'user', []) - await setup(database, 'profile', []) - await setup(database, 'post', []) - - for (const user of userTable) { - await database.upsert('user', [{ - ...userTable.find(u => u.id === user.id)!, - profile: profileTable.find(profile => profile.id === user.id), - }] as any) - } - - await expect(database.select('user', {}, { profile: true }).execute()).to.eventually.have.shape( - userTable.map(user => ({ - ...user, - profile: profileTable.find(profile => profile.id === user.id), - })), - ) - }) - - it('create oneToMany', async () => { - await setup(database, 'user', userTable) - await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - - posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post1' })) - posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post2' })) - - await database.set('user', 2, { - posts: { - $create: [ - { content: 'post1' }, - { content: 'post2' }, - ], - }, - }) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - - posts.push(database.tables['post'].create({ id: 101, author: { id: 1 }, content: 'post101' })) - await database.set('user', 1, row => ({ - value: $.add(row.id, 98), - posts: { - $create: { id: 101, content: 'post101' }, - }, - })) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - }) - - it('set oneToMany', async () => { - await setup(database, 'user', userTable) - await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - - posts[0].score = 2 - posts[1].score = 3 - await database.set('user', 1, row => ({ - posts: { - $set: r => ({ - score: $.add(row.id, r.id), - }), - }, - })) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - - posts[1].score = 13 - await database.set('user', 1, row => ({ - posts: { - $set: { - where: { score: { $gt: 2 } }, - update: r => ({ score: $.add(r.score, 10) }), - }, - }, - })) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - }) - - it('delete oneToMany', async () => { - await setup(database, 'user', userTable) - await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - - posts.splice(0, 1) - await database.set('user', {}, row => ({ - posts: { - $remove: r => $.eq(r.id, row.id), - }, - })) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - }) - - it('override oneToMany', async () => { - await setup(database, 'user', userTable) - await setup(database, 'profile', profileTable) - const posts = await setup(database, 'post', postTable) - - posts[0].score = 2 - posts[1].score = 3 - await database.set('user', 1, row => ({ - posts: posts.slice(0, 2), - })) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - - posts[0].score = 4 - posts[1].score = 5 - await database.upsert('user', [{ - id: 1, - posts: posts.slice(0, 2), - }]) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - }) - - ignoreNullObject && it('connect / disconnect oneToMany', async () => { - await setup(database, 'user', userTable) - await setup(database, 'profile', profileTable) - await setup(database, 'post', postTable) - - await database.set('user', 1, { - posts: { - $disconnect: {}, - $connect: { id: 3 }, - }, - }) - await expect(database.get('user', 1, ['posts'])).to.eventually.have.shape([{ - posts: [ - { id: 3 }, - ], - }]) - }) - - it('connect / disconnect manyToMany', async () => { - await setup(database, 'user', userTable) - await setup(database, 'profile', profileTable) - await setup(database, 'post', postTable) - await setup(database, 'tag', tagTable) - - await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable.map(x => ({ - post_id: x.postId, - tag_id: x.tagId, - }))) - - await database.set('post', 2, { - tags: { - $disconnect: {}, - }, - }) - await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').deep.equal([]) - - await database.set('post', 2, row => ({ - tags: { - $connect: r => $.eq(r.id, row.id), - }, - })) - await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([{ - id: 2, - }]) - }) - - it('query relation', async () => { - await setup(database, 'user', userTable) - const posts = await setup(database, 'post', postTable) - await setup(database, 'tag', tagTable) - await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable.map(x => ({ - post_id: x.postId, - tag_id: x.tagId, - }))) - - posts.filter(post => post2TagTable.some(p2t => p2t.postId === post.id && p2t.tagId === 1)).forEach(post => post.score! += 10) - await database.set('post', { - tags: { - $some: { - id: 1, - }, - }, - }, row => ({ - score: $.add(row.score, 10), - })) - await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - }) - } - - export function misc(database: Database) { - it('unsupported', async () => { - await setup(database, 'user', userTable) - await setup(database, 'post', postTable) - await setup(database, 'tag', tagTable) - - await expect(database.set('post', 1, { - tags: [], - })).to.eventually.be.rejected - - await expect(database.set('post', 1, { - tags: { - $remove: {}, - }, - })).to.eventually.be.rejected - - await expect(database.set('post', 1, { - tags: { - $set: {}, - }, - })).to.eventually.be.rejected - - await expect(database.set('post', 1, { - tags: { - $create: {}, - }, - })).to.eventually.be.rejected - }) - } + // export function query(database: Database) { + // it('oneToOne / manyToOne', async () => { + // const users = await setup(database, 'user', userTable) + // const profiles = await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + + // await expect(database.get('user', { + // profile: { + // user: { + // id: 1, + // }, + // }, + // })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ + // ...user, + // profile: profiles.find(profile => profile.id === user.id), + // }))) + + // await expect(database.get('user', row => $.query(row, { + // profile: r => $.eq(r.id, row.id), + // }))).to.eventually.have.shape(users.map(user => ({ + // ...user, + // profile: profiles.find(profile => profile.id === user.id), + // }))) + + // await expect(database.get('user', { + // profile: { + // user: { + // value: 1, + // }, + // }, + // })).to.eventually.have.shape(users.slice(1, 2).map(user => ({ + // ...user, + // profile: profiles.find(profile => profile.id === user.id), + // }))) + + // await expect(database.get('post', { + // author: { + // id: 1, + // }, + // })).to.eventually.have.shape(posts.map(post => ({ + // ...post, + // author: users.find(user => post.author?.id === user.id), + // })).filter(post => post.author?.id === 1)) + + // await expect(database.get('post', { + // author: { + // id: 1, + // value: 1, + // }, + // })).to.eventually.have.length(0) + // }) + + // it('oneToMany', async () => { + // const users = await setup(database, 'user', userTable) + // const profiles = await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + + // await expect(database.get('user', { + // posts: { + // $some: { + // author: { + // id: 1, + // }, + // }, + // }, + // })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ + // ...user, + // posts: posts.filter(post => post.author?.id === user.id), + // }))) + + // await expect(database.get('user', { + // posts: { + // $some: row => $.eq(row.id, 1), + // }, + // })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ + // ...user, + // posts: posts.filter(post => post.author?.id === user.id), + // }))) + + // await expect(database.get('user', { + // posts: { + // $none: { + // author: { + // id: 1, + // }, + // }, + // }, + // })).to.eventually.have.shape(users.slice(1).map(user => ({ + // ...user, + // posts: posts.filter(post => post.author?.id === user.id), + // }))) + + // await expect(database.get('user', { + // posts: { + // $every: { + // author: { + // id: 1, + // }, + // }, + // }, + // })).to.eventually.have.shape([users[0], users[2]].map(user => ({ + // ...user, + // posts: posts.filter(post => post.author?.id === user.id), + // }))) + // }) + + // it('manyToMany', async () => { + // const users = await setup(database, 'user', userTable) + // const profiles = await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + // const tags = await setup(database, 'tag', tagTable) + // const post2tags = await setup(database, 'post2tag', post2TagTable) + // const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + + // await expect(database.get('post', { + // tags: { + // $some: { + // id: 1, + // }, + // }, + // })).to.eventually.have.shape(posts.slice(0, 2).map(post => ({ + // ...post, + // tags: post2tags.filter(p2t => p2t.post?.id === post.id) + // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + // .filter(tag => tag), + // }))) + + // await expect(database.get('post', { + // tags: { + // $none: { + // id: 1, + // }, + // }, + // })).to.eventually.have.shape(posts.slice(2).map(post => ({ + // ...post, + // tags: post2tags.filter(p2t => p2t.post?.id === post.id) + // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + // .filter(tag => tag), + // }))) + + // await expect(database.get('post', { + // tags: { + // $every: { + // id: 3, + // }, + // }, + // })).to.eventually.have.shape(posts.slice(2, 3).map(post => ({ + // ...post, + // tags: post2tags.filter(p2t => p2t.post?.id === post.id) + // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + // .filter(tag => tag), + // }))) + + // await expect(database.get('post', { + // tags: { + // $some: 1, + // $none: [3], + // $every: {}, + // }, + // })).to.eventually.have.shape(posts.slice(0, 1).map(post => ({ + // ...post, + // tags: post2tags.filter(p2t => p2t.post?.id === post.id) + // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + // .filter(tag => tag), + // }))) + // }) + + // it('nested query', async () => { + // const users = await setup(database, 'user', userTable) + // const profiles = await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + // const tags = await setup(database, 'tag', tagTable) + // const post2tags = await setup(database, 'post2tag', post2TagTable) + // const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + + // await expect(database.get('user', { + // posts: { + // $some: { + // tags: { + // $some: { + // id: 1, + // }, + // }, + // }, + // }, + // })).to.eventually.have.shape([users[0]].map(user => ({ + // ...user, + // posts: posts.filter(post => post.author?.id === user.id), + // }))) + + // await expect(database.get('tag', { + // posts: { + // $some: { + // author: { + // id: 2, + // }, + // }, + // }, + // })).to.eventually.have.shape([tags[2]].map(tag => ({ + // ...tag, + // posts: post2tags.filter(p2t => p2t.tag?.id === tag.id) + // .map(p2t => posts.find(post => post.id === p2t.post?.id)) + // .filter(post => post), + // }))) + // }) + + // it('omit query', async () => { + // const users = await setup(database, 'user', userTable) + // const profiles = await setup(database, 'profile', profileTable) + + // await expect(database.get('user', { id: 2 }, ['id', 'profile'])).to.eventually.have.shape( + // [users[1]].map(user => ({ + // id: user.id, + // profile: profiles.find(profile => user.id === profile.id), + // })), + // ) + // }) + + // it('existence', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'profile', profileTable) + // await setup(database, 'post', postTable) + + // await expect(database.select('user', { successor: null }, null).execute()).to.eventually.have.shape([ + // { id: 1 }, + // { id: 3 }, + // ]) + + // await expect(database.select('user', { predecessor: null }, null).execute()).to.eventually.have.shape([ + // { id: 2 }, + // { id: 3 }, + // ]) + + // await database.set('user', 1, { profile: null }) + // await expect(database.select('user', { profile: null }, null).execute()).to.eventually.have.shape([ + // { id: 1 }, + // ]) + + // await database.set('user', 2, { + // posts: { + // $disconnect: { + // id: 3, + // }, + // }, + // }) + // await expect(database.select('post', { author: null }, null).execute()).to.eventually.have.shape([ + // { id: 3 }, + // ]) + // await expect(database.select('user', { + // posts: { + // $every: { + // author: null, + // }, + // }, + // }, null).execute()).to.eventually.have.shape([ + // { id: 2 }, + // { id: 3 }, + // ]) + // }) + + // it('manyToOne fallbacck', async () => { + // await setup(database, 'user', []) + // await setup(database, 'profile', []) + // await setup(database, 'post', []) + + // await database.create('post', { + // id: 1, + // content: 'new post', + // author: { + // id: 2, + // } + // }) + + // await expect(database.get('post', 1, ['author'])).to.eventually.have.shape([{ + // author: { + // id: 2, + // }, + // }]) + + // await database.create('user', { + // id: 2, + // value: 123, + // }) + + // await expect(database.get('post', 1, ['author'])).to.eventually.have.shape([{ + // author: { + // id: 2, + // value: 123, + // }, + // }]) + // }) + // } + + // export function create(database: Database) { + // it('basic support', async () => { + // await setup(database, 'user', []) + // await setup(database, 'profile', []) + // await setup(database, 'post', []) + + // for (const user of userTable) { + // await expect(database.create('user', { + // ...user, + // profile: { + // ...profileTable.find(profile => profile.id === user.id)!, + // }, + // posts: postTable.filter(post => post.author?.id === user.id), + // })).to.eventually.have.shape(user) + // } + + // await expect(database.select('profile', {}, { user: true }).execute()).to.eventually.have.shape( + // profileTable.map(profile => ({ + // ...profile, + // user: userTable.find(user => user.id === profile.id), + // })), + // ) + + // await expect(database.select('user', {}, { profile: true, posts: true }).execute()).to.eventually.have.shape( + // userTable.map(user => ({ + // ...user, + // profile: profileTable.find(profile => profile.id === user.id), + // posts: postTable.filter(post => post.author?.id === user.id), + // })), + // ) + // }) + + // it('oneToMany', async () => { + // await setup(database, 'user', []) + // await setup(database, 'profile', []) + // await setup(database, 'post', []) + + // for (const user of userTable) { + // await database.create('user', { + // ...userTable.find(u => u.id === user.id)!, + // posts: postTable.filter(post => post.author?.id === user.id), + // }) + // } + + // await expect(database.select('user', {}, { posts: true }).execute()).to.eventually.have.shape( + // userTable.map(user => ({ + // ...user, + // posts: postTable.filter(post => post.author?.id === user.id), + // })), + // ) + // }) + + // it('manyToOne', async () => { + // const users = await setup(database, 'user', []) + // await setup(database, 'post', []) + + // users.push({ id: 1, value: 2 }) + + // await database.create('post', { + // id: 1, + // content: 'post2', + // author: { + // $create: { + // id: 1, + // value: 2, + // }, + // }, + // }) + // await expect(database.get('user', {})).to.eventually.have.shape(users) + + // users[0].value = 3 + // await database.create('post', { + // id: 2, + // content: 'post3', + // author: { + // $create: { + // id: 1, + // value: 3, + // }, + // }, + // }) + // await expect(database.get('user', {})).to.eventually.have.shape(users) + + // await database.create('post', { + // id: 3, + // content: 'post4', + // author: { + // id: 1, + // value: 30, + // }, + // }) + // await expect(database.get('user', {})).to.eventually.have.shape(users) + // await expect(database.get('post', {}, { include: { author: true } })).to.eventually.have.shape([ + // { id: 1, content: 'post2', author: { id: 1, value: 3 } }, + // { id: 2, content: 'post3', author: { id: 1, value: 3 } }, + // { id: 3, content: 'post4', author: { id: 1, value: 3 } }, + // ]) + // }) + + + // it('upsert manyToOne', async () => { + // const users = await setup(database, 'user', []) + // await setup(database, 'post', []) + + // users.push({ id: 1, value: 2 }) + + // await database.upsert('post', [{ + // id: 1, + // content: 'post2', + // author: { + // $create: { + // id: 1, + // value: 2, + // }, + // }, + // }]) + // await expect(database.get('user', {})).to.eventually.have.shape(users) + + // users[0].value = 3 + // await database.upsert('post', [{ + // id: 2, + // content: 'post3', + // author: { + // $create: { + // id: 1, + // value: 3, + // }, + // }, + // }, + // { + // id: 3, + // content: 'post4', + // author: { + // id: 1, + // value: 30, + // }, + // }]) + // await expect(database.get('user', {})).to.eventually.have.shape(users) + // await expect(database.get('post', {}, { include: { author: true } })).to.eventually.have.shape([ + // { id: 1, content: 'post2', author: { id: 1, value: 3 } }, + // { id: 2, content: 'post3', author: { id: 1, value: 3 } }, + // { id: 3, content: 'post4', author: { id: 1, value: 3 } }, + // ]) + // }) + + // it('manyToMany', async () => { + // await setup(database, 'user', []) + // await setup(database, 'post', []) + // await setup(database, 'tag', []) + + // for (const user of userTable) { + // await database.create('user', { + // ...userTable.find(u => u.id === user.id)!, + // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ + // ...post, + // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), + // })), + // }) + // } + + // await expect(database.select('user', {}, { posts: { tags: true } }).execute()).to.eventually.have.shape( + // userTable.map(user => ({ + // ...user, + // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ + // ...post, + // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)), + // })), + // })), + // ) + // }) + + // it('upsert manyToMany', async () => { + // await setup(database, 'user', []) + // await setup(database, 'post', []) + // await setup(database, 'tag', []) + + // await database.upsert('user', userTable.map(user => ({ + // ...userTable.find(u => u.id === user.id)!, + // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ + // ...post, + // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), + // })), + // }))) + + // await expect(database.select('user', {}, { posts: { tags: true } }).execute()).to.eventually.have.shape( + // userTable.map(user => ({ + // ...user, + // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ + // ...post, + // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)), + // })), + // })), + // ) + // }) + // } + + // export function modify(database: Database, options: RelationOptions = {}) { + // const { ignoreNullObject = true } = options + + // it('oneToOne / manyToOne', async () => { + // const users = await setup(database, 'user', userTable) + // const profiles = await setup(database, 'profile', profileTable) + // await setup(database, 'post', postTable) + + // profiles.splice(2, 1) + // await database.set('user', 3, { + // profile: null, + // }) + // await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + + // profiles.push(database.tables['profile'].create({ id: 3, name: 'Reborn' })) + // await database.set('user', 3, { + // profile: { + // name: 'Reborn', + // }, + // }) + // await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + + // users[0].value = 99 + // await database.set('post', 1, { + // author: { + // value: 99, + // }, + // }) + // await expect(database.get('user', {})).to.eventually.have.deep.members(users) + + // profiles.splice(2, 1) + // await database.set('user', 3, { + // profile: null, + // }) + // await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + + // users.push({ id: 100, value: 200, successor: { id: undefined } } as any) + // await database.set('post', 1, { + // author: { + // id: 100, + // value: 200, + // }, + // }) + // await expect(database.get('user', {})).to.eventually.have.deep.members(users) + // }) + + // it('upsert', async () => { + // await setup(database, 'user', []) + // await setup(database, 'profile', []) + // await setup(database, 'post', []) + + // for (const user of userTable) { + // await database.upsert('user', [{ + // ...userTable.find(u => u.id === user.id)!, + // profile: profileTable.find(profile => profile.id === user.id), + // }] as any) + // } + + // await expect(database.select('user', {}, { profile: true }).execute()).to.eventually.have.shape( + // userTable.map(user => ({ + // ...user, + // profile: profileTable.find(profile => profile.id === user.id), + // })), + // ) + // }) + + // it('create oneToMany', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + + // posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post1' })) + // posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post2' })) + + // await database.set('user', 2, { + // posts: { + // $create: [ + // { content: 'post1' }, + // { content: 'post2' }, + // ], + // }, + // }) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + + // posts.push(database.tables['post'].create({ id: 101, author: { id: 1 }, content: 'post101' })) + // await database.set('user', 1, row => ({ + // value: $.add(row.id, 98), + // posts: { + // $create: { id: 101, content: 'post101' }, + // }, + // })) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + // }) + + // it('set oneToMany', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + + // posts[0].score = 2 + // posts[1].score = 3 + // await database.set('user', 1, row => ({ + // posts: { + // $set: r => ({ + // score: $.add(row.id, r.id), + // }), + // }, + // })) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + + // posts[1].score = 13 + // await database.set('user', 1, row => ({ + // posts: { + // $set: { + // where: { score: { $gt: 2 } }, + // update: r => ({ score: $.add(r.score, 10) }), + // }, + // }, + // })) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + // }) + + // it('delete oneToMany', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + + // posts.splice(0, 1) + // await database.set('user', {}, row => ({ + // posts: { + // $remove: r => $.eq(r.id, row.id), + // }, + // })) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + // }) + + // it('override oneToMany', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'profile', profileTable) + // const posts = await setup(database, 'post', postTable) + + // posts[0].score = 2 + // posts[1].score = 3 + // await database.set('user', 1, row => ({ + // posts: posts.slice(0, 2), + // })) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + + // posts[0].score = 4 + // posts[1].score = 5 + // await database.upsert('user', [{ + // id: 1, + // posts: posts.slice(0, 2), + // }]) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + // }) + + // ignoreNullObject && it('connect / disconnect oneToMany', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'profile', profileTable) + // await setup(database, 'post', postTable) + + // await database.set('user', 1, { + // posts: { + // $disconnect: {}, + // $connect: { id: 3 }, + // }, + // }) + // await expect(database.get('user', 1, ['posts'])).to.eventually.have.shape([{ + // posts: [ + // { id: 3 }, + // ], + // }]) + // }) + + // it('connect / disconnect manyToMany', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'profile', profileTable) + // await setup(database, 'post', postTable) + // await setup(database, 'tag', tagTable) + + // await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + + // await database.set('post', 2, { + // tags: { + // $disconnect: {}, + // }, + // }) + // await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').deep.equal([]) + + // await database.set('post', 2, row => ({ + // tags: { + // $connect: r => $.eq(r.id, row.id), + // }, + // })) + // await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([{ + // id: 2, + // }]) + // }) + + // it('query relation', async () => { + // await setup(database, 'user', userTable) + // const posts = await setup(database, 'post', postTable) + // await setup(database, 'tag', tagTable) + // await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + + // posts.filter(post => post2TagTable.some(p2t => p2t.post?.id === post.id && p2t.tag?.id === 1)).forEach(post => post.score! += 10) + // await database.set('post', { + // tags: { + // $some: { + // id: 1, + // }, + // }, + // }, row => ({ + // score: $.add(row.score, 10), + // })) + // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + // }) + + // it('shared manyToMany', async () => { + // await setup(database, 'login', [ + // { id: '1', platform: 'sandbox', name: 'Bot1' }, + // { id: '2', platform: 'sandbox', name: 'Bot2' }, + // { id: '3', platform: 'sandbox', name: 'Bot3' }, + // ]) + // await setup(database, 'guild', [ + // { id: '1', platform2: 'sandbox', name: 'Guild1' }, + // { id: '2', platform2: 'sandbox', name: 'Guild2' }, + // { id: '3', platform2: 'sandbox', name: 'Guild3' }, + // ]) + // await setup(database, Relation.buildAssociationTable('login', 'guild') as any, []) + + // await database.set('login', { + // id: '1', + // platform: 'sandbox', + // }, { + // guilds: { + // $connect: { + // id: { + // $or: ['1', '2'] + // }, + // }, + // }, + // }) + // await expect(database.get('login', { + // id: '1', + // platform: 'sandbox', + // }, ['guilds'])).to.eventually.have.nested.property('[0].guilds').with.length(2) + + // // await database.set('post', 2, row => ({ + // // tags: { + // // $connect: r => $.eq(r.id, row.id), + // // }, + // // })) + // // await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([{ + // // id: 2, + // // }]) + // }) + + // } + + // export function misc(database: Database) { + // it('unsupported', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'post', postTable) + // await setup(database, 'tag', tagTable) + + // await expect(database.set('post', 1, { + // tags: [], + // })).to.eventually.be.rejected + + // await expect(database.set('post', 1, { + // tags: { + // $remove: {}, + // }, + // })).to.eventually.be.rejected + + // await expect(database.set('post', 1, { + // tags: { + // $set: {}, + // }, + // })).to.eventually.be.rejected + + // await expect(database.set('post', 1, { + // tags: { + // $create: {}, + // }, + // })).to.eventually.be.rejected + // }) + // } } export default RelationTests From 8e8ac3193aa06227666173b9f9512cd232e7d16b Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Thu, 23 May 2024 00:50:19 +0800 Subject: [PATCH 02/17] stage --- packages/core/src/database.ts | 243 +++++++++++++++++++-------------- packages/core/src/driver.ts | 8 +- packages/core/src/eval.ts | 4 +- packages/core/src/model.ts | 24 +++- packages/core/src/selection.ts | 19 ++- packages/tests/src/relation.ts | 8 ++ 6 files changed, 184 insertions(+), 122 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index b06d72b9..ceaa5ecc 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -1,6 +1,6 @@ -import { defineProperty, Dict, filterKeys, makeArray, mapValues, MaybeArray, noop, omit } from 'cosmokit' +import { defineProperty, Dict, filterKeys, isNullable, makeArray, mapValues, MaybeArray, noop, omit } from 'cosmokit' import { Context, Service, Spread } from 'cordis' -import { DeepPartial, FlatKeys, FlatPick, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' +import { AtomicTypes, DeepPartial, FlatKeys, FlatPick, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' import { Selection } from './selection.ts' import { Field, Model, Relation } from './model.ts' import { Driver } from './driver.ts' @@ -44,6 +44,13 @@ export namespace Join2 { export type Predicate> = (args: Parameters) => Eval.Expr } +export type Create = + | T extends Values ? T + : T extends (infer U)[] ? DeepPartial[] + : T extends Values ? { $create: { [K in keyof T]?: Create } } | { [K in keyof T]?: Create } + : T extends object ? { [K in keyof T]?: Create } + : T + export class Database extends Service { static [Service.provide] = 'model' static [Service.immediate] = true @@ -131,10 +138,13 @@ export class Database extends Servi } else if (relation.type === 'manyToMany') { const assocTable = Relation.buildAssociationTable(relation.table, name) if (this.tables[assocTable]) return - const fields = relation.fields.map(x => [Relation.buildAssociationKey(x, name), model.fields[x]?.deftype] as const) + const shared = Object.keys(relation.shared).map(x => [x, model.fields[x]!.deftype] as const) + const fields = relation.fields.map(x => [Relation.buildAssociationKey(x, name), model.fields[x]!.deftype] as const) const references = relation.references.map((x, i) => [Relation.buildAssociationKey(x, relation.table), fields[i][1]] as const) this.extend(assocTable as any, { - ...Object.fromEntries([...fields, ...references]), + ...Object.fromEntries([...shared, ...fields, ...references]), + // [`${relation.table}.extraFields`]: 'string', + [`${name}.extraFields`]: 'string', [name]: { type: 'manyToOne', table: name, @@ -148,10 +158,14 @@ export class Database extends Servi references: relation.fields, }, } as any, { - primary: [...fields.map(x => x[0]), ...references.map(x => x[0])], + primary: [...shared, ...fields, ...references].map(x => x[0]), }) } }) + // use relation field as primary + if (Array.isArray(model.primary) && model.primary.every(key => model.fields[key]?.relation)) { + model.primary = model.primary.map(key => model.fields[key]!.relation!.fields).flat() + } this.prepareTasks[name] = this.prepare(name) ;(this.ctx as Context).emit('model', name) } @@ -398,12 +412,13 @@ export class Database extends Servi async get, P extends FlatKeys = any>( table: K, query: Query, - cursor?: Driver.Cursor

, + cursor?: Driver.Cursor, P>, ): Promise[]> async get>(table: K, query: Query, cursor?: any) { - const fields = Array.isArray(cursor) ? cursor : cursor?.fields - return this.select(table, query, fields && Object.fromEntries(fields.map(x => [x, true])) as any).execute(cursor) as any + let fields = Array.isArray(cursor) ? cursor : cursor?.fields + fields = fields ? Object.fromEntries(fields.map(x => [x, true])) : cursor?.include + return this.select(table, query, fields).execute(cursor) as any } async eval, T>(table: K, expr: Selection.Callback, query?: Query): Promise { @@ -466,10 +481,10 @@ export class Database extends Servi return sel._action('remove').execute() } - async create>(table: K, data: DeepPartial): Promise + async create>(table: K, data: Create): Promise async create>(table: K, data: any): Promise { const sel = this.select(table) - const { primary, autoInc, fields } = sel.model + const { primary, autoInc } = sel.model if (!autoInc) { const keys = makeArray(primary) if (keys.some(key => getCell(data, key) === undefined)) { @@ -477,50 +492,17 @@ export class Database extends Servi } } - const tasks: any[] = [] - for (const key in data) { - if (data[key] && this.tables[table].fields[key]?.relation) { - const relation = this.tables[table].fields[key].relation - if (relation.type === 'oneToOne' && relation.required) { - const mergedData = { ...data[key] } - for (const k in relation.fields) { - mergedData[relation.references[k]] = getCell(data, relation.fields[k]) - } - tasks.push([relation.table, [mergedData], relation.references]) - } else if (relation.type === 'oneToMany' && Array.isArray(data[key])) { - const mergedData = data[key].map(row => { - const mergedData = { ...row } - for (const k in relation.fields) { - mergedData[relation.references[k]] = getCell(data, relation.fields[k]) - } - return mergedData - }) - - tasks.push([relation.table, mergedData]) - } else { - // handle shadowed fields - data = { - ...omit(data, [key]), - ...Object.fromEntries(Object.entries(data[key]).map(([k, v]) => { - if (!fields[`${key}.${k}`]) { - throw new Error(`field ${key}.${k} does not exist`) - } - return [`${key}.${k}`, v] - })), - } - continue - } - data = omit(data, [key]) as any - } - } - - if (tasks.length) { + const tasks = this.transformUpsert(sel, table, [data]) + if (Object.keys(tasks).length > 1) { return this.ensureTransaction(async (database) => { - for (const [table, data, keys] of tasks) { - await database.upsert(table, data, keys) + for (const [key, { table, upsert, keys }] of Object.entries(tasks)) { + if (!key) continue + await database.upsert(table as any, upsert, keys as any) } - return database.create(table, data) + return database.create(table, tasks[''].upsert[0]) }) + } else { + data = tasks[''].upsert[0] } return sel._action('create', sel.model.create(data)).execute() } @@ -533,61 +515,17 @@ export class Database extends Servi const sel = this.select(table) if (typeof upsert === 'function') upsert = upsert(sel.row) else { - const buildKey = (relation: Relation.Config) => [relation.table, ...relation.references].join('__') - const tasks: Dict<{ - table: string - upsert: any[] - keys?: string[] - }> = {} - - const upsert2 = (upsert as any[]).map(data => { - for (const key in data) { - if (data[key] && this.tables[table].fields[key]?.relation) { - const relation = this.tables[table].fields[key].relation - if (relation.type === 'oneToOne' && relation.required) { - const mergedData = { ...data[key] } - for (const k in relation.fields) { - mergedData[relation.references[k]] = data[relation.fields[k]] - } - ;(tasks[buildKey(relation)] ??= { - table: relation.table, - upsert: [], - keys: relation.references, - }).upsert.push(mergedData) - } else if (relation.type === 'oneToMany' && Array.isArray(data[key])) { - const mergedData = data[key].map(row => { - const mergedData = { ...row } - for (const k in relation.fields) { - mergedData[relation.references[k]] = data[relation.fields[k]] - } - return mergedData - }) - - ;(tasks[relation.table] ??= { table: relation.table, upsert: [] }).upsert.push(...mergedData) - } else { - // handle shadowed fields - data = { - ...omit(data, [key]), - ...Object.fromEntries(Object.entries(data[key]).map(([k, v]) => { - if (!sel.model.fields[`${key}.${k}`]) throw new Error(`field ${key}.${k} does not exist`) - return [`${key}.${k}`, v] - })), - } - continue - } - data = omit(data, [key]) as any - } - } - return data - }) - - if (Object.keys(tasks).length) { + const tasks = this.transformUpsert(sel, table, upsert, keys) + if (Object.keys(tasks).length > 1) { return this.ensureTransaction(async (database) => { - for (const { table, upsert, keys } of Object.values(tasks)) { + for (const [key, { table, upsert, keys }] of Object.entries(tasks)) { + if (!key) continue await database.upsert(table as any, upsert, keys as any) } - return database.upsert(table, upsert2) + return database.upsert(table, tasks[''].upsert, keys) }) + } else { + upsert = tasks[''].upsert } } upsert = upsert.map(item => sel.model.format(item)) @@ -743,6 +681,95 @@ export class Database extends Servi return { $expr: Eval.and(...results) } as any } + private transformUpsert(sel: Selection, table: string, upsert: any[], keys?: MaybeArray) { + const tasks: Dict<{ + table: string + upsert: any[] + keys: string[] + }> = {} + + const pushTask = (table: string, upsert: any[], keys?: MaybeArray, key?: string) => { + keys = makeArray(keys ?? this.tables[table].primary) + key ??= [table, ...keys].join('_$_') + ;(tasks[key] ??= { + table, + upsert: [], + keys, + }).upsert.push(...upsert) + } + + const upsert2 = upsert.map(data => { + for (const key in data) { + if (data[key] && this.tables[table].fields[key]?.relation) { + const relation = this.tables[table].fields[key].relation + if (relation.type === 'oneToOne' && relation.required) { + const mergedData = { ...data[key] } + for (const k in relation.fields) { + mergedData[relation.references[k]] = data[relation.fields[k]] + } + pushTask(relation.table, [mergedData], relation.references) + } else if (relation.type === 'manyToOne' && data[key].$create) { + pushTask(relation.table, [data[key].$create], relation.references) + data = { + ...omit(data, [key]), + ...Object.fromEntries(Object.entries(data[key].$create) + .filter(([k]) => sel.model.fields[`${key}.${k}`]) + .map(([k, v]) => [`${key}.${k}`, v])), + } + continue + } else if (relation.type === 'oneToMany' && Array.isArray(data[key])) { + const mergedData = data[key].map(row => { + const mergedData = { ...row } + for (const k in relation.fields) { + mergedData[relation.references[k]] = data[relation.fields[k]] + } + return mergedData + }) + pushTask(relation.table, mergedData) + } else if (relation.type === 'manyToMany') { + const assocTable: any = Relation.buildAssociationTable(table, relation.table) + const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) + const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) + pushTask(assocTable, data[key].map(r => ({ + ...Object.fromEntries(fields.map((k, i) => [k, getCell(data, relation.fields[i])])), + ...Object.fromEntries(references.map((k, i) => [k, getCell(r, relation.references[i])])), + }))) + pushTask(relation.table, data[key]) + } else { + // handle shadowed fields + data = { + ...omit(data, [key]), + ...Object.fromEntries(Object.entries(data[key]) + .filter(([k]) => sel.model.fields[`${key}.${k}`]) + .map(([k, v]) => [`${key}.${k}`, v])), + } + continue + } + data = omit(data, [key]) + } + } + return data + }) + pushTask(table, upsert2, keys, '') + Object.values(tasks).forEach(task => { + const result: any[] = [], added = new Set() + task.upsert.map(data => { + const primary = task.keys.map(k => getCell(data, k)) + if (primary.some(isNullable)) { + result.push(data) + } else { + const key = primary.join('_$_') + if (!added.has(key)) { + result.push(data) + added.add(key) + } + } + }) + task.upsert = result + }) + return tasks + } + private async processRelationUpdate(table: any, row: any, key: any, modifier: Relation.Modifier) { const relation: Relation.Config = this.tables[table].fields[key]!.relation! as any if (Array.isArray(modifier)) { @@ -796,7 +823,7 @@ export class Database extends Servi if (relation.type === 'oneToMany') { await this.set(relation.table, (r: any) => Eval.query(r, { - ...Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])), + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...(typeof modifier.$disconnect === 'function' ? { $expr: modifier.$disconnect } : modifier.$disconnect), } as any), Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, @@ -806,12 +833,12 @@ export class Database extends Servi const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) const rows = await this.select(assocTable, { - ...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])) as any, + ...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, [relation.table]: modifier.$disconnect, }, null).execute() await this.remove(assocTable, r => Eval.in( [...fields.map(x => r[x]), ...references.map(x => r[x])], - rows.map(r => [...fields.map(x => r[x]), ...references.map(x => r[x])]), + rows.map(r => [...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), )) } } @@ -825,8 +852,12 @@ export class Database extends Servi const assocTable: any = Relation.buildAssociationTable(table, relation.table) const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) - const rows = await this.get(relation.table, modifier.$connect) + const rows = await this.get(relation.table, (r: any) => Eval.query(r, { + ...Object.fromEntries(Object.entries(relation.shared).map(([k, v]) => [v, getCell(row, k)])), + ...(typeof modifier.$connect === 'function' ? { $expr: modifier.$connect(r) } : modifier.$connect), + }) as any) await this.upsert(assocTable, rows.map(r => ({ + ...mapValues(relation.shared, (v, k) => getCell(row, k)), ...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])), ...Object.fromEntries(references.map((k, i) => [k, r[relation.references[i] as any]])), })) as any) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index b98163e2..0b56746c 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -2,9 +2,10 @@ import { Awaitable, Dict, mapValues, remove } from 'cosmokit' import { Context, Logger } from 'cordis' import { Eval, Update } from './eval.ts' import { Direction, Modifier, Selection } from './selection.ts' -import { Field, Model } from './model.ts' +import { Field, Model, Relation } from './model.ts' import { Database } from './database.ts' import { Type } from './type.ts' +import { FlatKeys } from './utils.ts' export namespace Driver { export interface Stats { @@ -17,13 +18,14 @@ export namespace Driver { size: number } - export type Cursor = K[] | CursorOptions + export type Cursor = any> = K[] | CursorOptions - export interface CursorOptions { + export interface CursorOptions = any> { limit?: number offset?: number fields?: K[] sort?: Dict + include?: Relation.Include } export interface WriteResult { diff --git a/packages/core/src/eval.ts b/packages/core/src/eval.ts index 78c5dd5b..377e2c15 100644 --- a/packages/core/src/eval.ts +++ b/packages/core/src/eval.ts @@ -1,5 +1,5 @@ import { defineProperty, isNullable, mapValues } from 'cosmokit' -import { AtomicTypes, Comparable, Flatten, isComparable, isEmpty, makeRegExp, Row, Values } from './utils.ts' +import { AtomicTypes, Comparable, DeepPartial, Flatten, isComparable, isEmpty, makeRegExp, Row, Values } from './utils.ts' import { Type } from './type.ts' import { Field, Relation } from './model.ts' import { Query } from './query.ts' @@ -31,7 +31,7 @@ type UnevalObject = { export type Uneval = | U extends Values ? Eval.Term : U extends (infer T extends object)[] ? Relation.Modifier | Eval.Array - : U extends object ? Eval.Expr | UnevalObject> + : U extends object ? Eval.Expr | UnevalObject> | { $create: DeepPartial } : any export type Eval = diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 7185a55b..b474d826 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -22,6 +22,8 @@ export namespace Relation { table: T references: Keys[] fields: K[] + shared: Record> + mixin?: K[] required: boolean } @@ -31,10 +33,11 @@ export namespace Relation { target?: string references?: MaybeArray fields?: MaybeArray + shared?: MaybeArray | Record } export type Include = boolean | { - [P in keyof T]?: T[P] extends MaybeArray | undefined ? Include : never + [P in keyof T]?: T[P] extends MaybeArray | undefined ? U extends S ? Include : never : never } export type SetExpr = Row.Computed> | { @@ -51,14 +54,21 @@ export namespace Relation { } export function buildAssociationTable(...tables: [string, string]) { - return '_' + tables.sort().join('To') + return '_' + tables.sort().join('_') } export function buildAssociationKey(key: string, table: string) { - return `${table}_${key}` + return `${table}.${key}` + } + + export function transformAssoicationFields(fields: string[], table: string) { } export function parse(def: Definition, key: string, model: Model, relmodel: Model): [Config, Config] { + const shared = !def.shared ? {} + : typeof def.shared === 'string' ? { [def.shared]: def.shared } + : Array.isArray(def.shared) ? Object.fromEntries(def.shared.map(x => [x, x])) + : def.shared const fields = def.fields ?? ((model.name === relmodel.name || def.type === 'manyToOne' || (def.type === 'oneToOne' && !makeArray(relmodel.primary).every(key => !relmodel.fields[key]?.nullable))) ? makeArray(relmodel.primary).map(x => `${key}.${x}`) : model.primary) @@ -66,10 +76,15 @@ export namespace Relation { type: def.type, table: def.table ?? relmodel.name, fields: makeArray(fields), + shared: mapValues(shared, (_, k) => k), references: makeArray(def.references ?? relmodel.primary), required: def.type !== 'manyToOne' && model.name !== relmodel.name && makeArray(fields).every(key => !model.fields[key]?.nullable || makeArray(model.primary).includes(key)), } + Object.entries(shared).forEach(([k, v]) => { + relation.fields = relation.fields.filter(x => x !== k) + relation.references = relation.references.filter(x => x !== v) + }) const inverse: Config = { type: relation.type === 'oneToMany' ? 'manyToOne' : relation.type === 'manyToOne' ? 'oneToMany' @@ -77,6 +92,7 @@ export namespace Relation { table: model.name, fields: relation.references, references: relation.fields, + shared, required: relation.type !== 'oneToMany' && !relation.required && relation.references.every(key => !relmodel.fields[key]?.nullable || makeArray(relmodel.primary).includes(key)), } @@ -215,7 +231,7 @@ export namespace Field { } export function available(field?: Field) { - return !!field && !field.deprecated && !field.relation + return !!field && !field.deprecated && !field.relation && field.deftype !== 'expr' } } diff --git a/packages/core/src/selection.ts b/packages/core/src/selection.ts index 443c9fb5..41bfd6cb 100644 --- a/packages/core/src/selection.ts +++ b/packages/core/src/selection.ts @@ -1,9 +1,9 @@ -import { defineProperty, Dict, filterKeys } from 'cosmokit' +import { defineProperty, Dict, filterKeys, mapValues } from 'cosmokit' import { Driver } from './driver.ts' import { Eval, executeEval, isAggrExpr, isEvalExpr } from './eval.ts' import { Field, Model } from './model.ts' import { Query } from './query.ts' -import { FlatKeys, FlatPick, Flatten, Keys, randomId, Row } from './utils.ts' +import { FlatKeys, FlatPick, Flatten, getCell, Keys, randomId, Row } from './utils.ts' import { Type } from './type.ts' declare module './eval.ts' { @@ -61,7 +61,7 @@ const createRow = (ref: string, expr = {}, prefix = '', model?: Model) => new Pr } const row = createRow(ref, Eval('', [ref, `${prefix}${key}`], type), `${prefix}${key}.`, model) - if (Object.keys(model?.fields!).some(k => k.startsWith(`${prefix}${key}.`))) { + if (!field && Object.keys(model?.fields!).some(k => k.startsWith(`${prefix}${key}.`))) { return createRow(ref, Eval.object(row), `${prefix}${key}.`, model) } else { return row @@ -253,15 +253,20 @@ export class Selection extends Executable { ): Selection { const fields = Object.fromEntries(Object.entries(this.model.fields) .filter(([key, field]) => Field.available(field) && !key.startsWith(name + '.')) - .map(([key]) => [key, (row) => key.split('.').reduce((r, k) => r[k], row[this.ref])])) + .map(([key]) => [key, (row) => getCell(row[this.ref], key)])) + const joinFields = Object.fromEntries(Object.entries(selection.model.fields) + .filter(([key, field]) => Field.available(field) || Field.available(this.model.fields[`${name}.${key}`])) + .map(([key]) => [key, + (row) => Field.available(this.model.fields[`${name}.${key}`]) ? getCell(row[this.ref], `${name}.${key}`) : getCell(row[name], key), + ])) if (optional) { return this.driver.database .join({ [this.ref]: this as Selection, [name]: selection }, (t: any) => callback(t[this.ref], t[name]), { [this.ref]: false, [name]: true }) - .project({ ...fields, [name]: (row) => Eval.ignoreNull(row[name]) }) as any + .project({ ...fields, [name]: (row) => Eval.ignoreNull(Eval.object(mapValues(joinFields, x => x(row)))) }) as any } else { return this.driver.database .join({ [this.ref]: this as Selection, [name]: selection }, (t: any) => callback(t[this.ref], t[name])) - .project({ ...fields, [name]: (row) => Eval.ignoreNull(row[name]) }) as any + .project({ ...fields, [name]: (row) => Eval.ignoreNull(Eval.object(mapValues(joinFields, x => x(row)))) }) as any } } @@ -282,7 +287,7 @@ export class Selection extends Executable { } execute(): Promise - execute = any>(cursor?: Driver.Cursor): Promise[]> + execute = any>(cursor?: Driver.Cursor): Promise[]> execute(callback: Selection.Callback): Promise async execute(cursor?: any) { if (typeof cursor === 'function') { diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index 7a8fbfaf..e3220c92 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -95,6 +95,10 @@ function RelationTests(database: Database) { id: 'unsigned', score: 'unsigned', content: 'string', + extraFields: { + type: 'expr', + nullable: true, + }, author: { type: 'manyToOne', table: 'user', @@ -107,6 +111,10 @@ function RelationTests(database: Database) { database.extend('tag', { id: 'unsigned', name: 'string', + extraFields: { + type: 'expr', + nullable: true, + }, posts: { type: 'manyToMany', table: 'post', From 545e8fe41a7362bfd638dd72b370f75c569b6bc7 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Sun, 2 Jun 2024 19:25:45 +0800 Subject: [PATCH 03/17] feat: basic --- packages/core/src/database.ts | 524 ++++++---- packages/core/src/driver.ts | 5 + packages/core/src/eval.ts | 4 +- packages/core/src/model.ts | 40 +- packages/tests/src/index.ts | 16 +- packages/tests/src/relation.ts | 1777 ++++++++++++++++++-------------- 6 files changed, 1337 insertions(+), 1029 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index ceaa5ecc..0d314fc7 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -1,10 +1,10 @@ -import { defineProperty, Dict, filterKeys, isNullable, makeArray, mapValues, MaybeArray, noop, omit } from 'cosmokit' +import { defineProperty, Dict, filterKeys, makeArray, mapValues, MaybeArray, noop, omit, pick } from 'cosmokit' import { Context, Service, Spread } from 'cordis' -import { AtomicTypes, DeepPartial, FlatKeys, FlatPick, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' +import { AtomicTypes, DeepPartial, FlatKeys, FlatPick, Flatten, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' import { Selection } from './selection.ts' import { Field, Model, Relation } from './model.ts' import { Driver } from './driver.ts' -import { Eval, Update } from './eval.ts' +import { Eval, isEvalExpr, Update } from './eval.ts' import { Query } from './query.ts' import { Type } from './type.ts' @@ -46,8 +46,19 @@ export namespace Join2 { export type Create = | T extends Values ? T + : T extends Values[] ? { [K in keyof T]?: Create } | + { + $create?: MaybeArray<{ [K in keyof T]?: Create }> + $upsert?: MaybeArray<{ [K in keyof T]?: Create }> + $connect?: Query.Expr> + } + : T extends Values ? { [K in keyof T]?: Create } | + { + $create?: { [K in keyof T]?: Create } + $upsert?: { [K in keyof T]?: Create } + $connect?: Query.Expr> + } : T extends (infer U)[] ? DeepPartial[] - : T extends Values ? { $create: { [K in keyof T]?: Create } } | { [K in keyof T]?: Create } : T extends object ? { [K in keyof T]?: Create } : T @@ -138,24 +149,22 @@ export class Database extends Servi } else if (relation.type === 'manyToMany') { const assocTable = Relation.buildAssociationTable(relation.table, name) if (this.tables[assocTable]) return - const shared = Object.keys(relation.shared).map(x => [x, model.fields[x]!.deftype] as const) + const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), model.fields[x]!.deftype] as const) const fields = relation.fields.map(x => [Relation.buildAssociationKey(x, name), model.fields[x]!.deftype] as const) const references = relation.references.map((x, i) => [Relation.buildAssociationKey(x, relation.table), fields[i][1]] as const) this.extend(assocTable as any, { ...Object.fromEntries([...shared, ...fields, ...references]), - // [`${relation.table}.extraFields`]: 'string', - [`${name}.extraFields`]: 'string', [name]: { type: 'manyToOne', table: name, - fields: fields.map(x => x[0]), - references: relation.references, + fields: [...shared, ...fields].map(x => x[0]), + references: [...Object.keys(relation.shared), ...relation.references], }, [relation.table]: { type: 'manyToOne', table: relation.table, - fields: references.map(x => x[0]), - references: relation.fields, + fields: [...shared, ...references].map(x => x[0]), + references: [...Object.values(relation.shared), ...relation.fields], }, } as any, { primary: [...shared, ...fields, ...references].map(x => x[0]), @@ -352,7 +361,12 @@ export class Database extends Servi } else if (relation.type === 'manyToMany') { const assocTable: any = Relation.buildAssociationTable(relation.table, table) const references = relation.fields.map(x => Relation.buildAssociationKey(x, table)) + const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { + field: x, + reference: y, + }] as const) sel = whereOnly ? sel : sel.join(key, this.select(assocTable, {}, { [relation.table]: cursor[key] } as any), (self, other) => Eval.and( + ...shared.map(([k, v]) => Eval.eq(self[v.field], other[k])), ...relation.fields.map((k, i) => Eval.eq(self[k], other[references[i]])), ), true) sel = applyQuery(sel, key) @@ -446,26 +460,8 @@ export class Database extends Servi const rows = await database.get(table, query) let baseUpdate = omit(update, relations.map(([key]) => key) as any) baseUpdate = sel.model.format(baseUpdate) - for (const [key, relation] of relations) { - if (relation.type === 'oneToOne') { - if (update[key] === null) { - await Promise.all(rows.map(row => database.remove(relation.table, - Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, - ))) - } else { - await database.upsert(relation.table, rows.map(row => ({ - ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), - ...rawupdate(row as any)[key], - })), relation.references as any) - } - } else if (relation.type === 'manyToOne') { - await database.upsert(relation.table, rows.map(row => ({ - ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), - ...rawupdate(row as any)[key], - })), relation.references as any) - } else if (relation.type === 'oneToMany' || relation.type === 'manyToMany') { - await Promise.all(rows.map(row => this.processRelationUpdate(table, row, key, rawupdate(row as any)[key]))) - } + for (const [key] of relations) { + await Promise.all(rows.map(row => this.processRelationUpdate(table, row, key, rawupdate(row as any)[key]))) } return Object.keys(baseUpdate).length === 0 ? {} : await sel._action('set', baseUpdate).execute() }) @@ -481,30 +477,121 @@ export class Database extends Servi return sel._action('remove').execute() } - async create>(table: K, data: Create): Promise - async create>(table: K, data: any): Promise { + async create>(table: K, data: Create, options?: Driver.CreateOptions): Promise + async create>(table: K, data: any, options?: any): Promise { const sel = this.select(table) - const { primary, autoInc } = sel.model - if (!autoInc) { - const keys = makeArray(primary) - if (keys.some(key => getCell(data, key) === undefined)) { - throw new Error('missing primary key') + + data = { ...data } + const tasks = [''] + for (const key in data) { + if (data[key] && this.tables[table].fields[key]?.relation) { + const relation = this.tables[table].fields[key].relation + if (relation.type === 'oneToOne' && relation.required) tasks.push(key) + else if (relation.type === 'oneToMany') tasks.push(key) + else if (relation.type === 'manyToOne' && isEvalExpr(data[key])) tasks.unshift(key) + else if (relation.type === 'manyToMany') tasks.push(key) } } - const tasks = this.transformUpsert(sel, table, [data]) - if (Object.keys(tasks).length > 1) { - return this.ensureTransaction(async (database) => { - for (const [key, { table, upsert, keys }] of Object.entries(tasks)) { - if (!key) continue - await database.upsert(table as any, upsert, keys as any) + for (const key of tasks) { + if (!key) { + // create the plain data, with or without upsert + const { primary, autoInc } = sel.model, keys = makeArray(primary) + if (keys.some(key => getCell(data, key) === undefined)) { + if (!autoInc) { + throw new Error('missing primary key') + } else { + (options ??= {}).upsert = false + } } - return database.create(table, tasks[''].upsert[0]) - }) - } else { - data = tasks[''].upsert[0] + if (options?.upsert) { + await sel._action('upsert', [sel.model.format(omit(data, tasks))], keys).execute() + } else { + Object.assign(data, await sel._action('create', sel.model.create(omit(data, tasks))).execute()) + } + continue + } + const value = data[key] + const relation = this.tables[table].fields[key]!.relation! + if (relation.type === 'oneToOne') { + await this.create(relation.table as any, { + ...value.$create ?? value, + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), + } as any, { upsert: true }) + // if (value.$create) { + + // } else if (value.$upsert) { + + // } else if (value.$connect) { + + // } + } else if (relation.type === 'oneToMany') { + if (value.$create || !isEvalExpr(value)) { + for (const item of makeArray(value.$create ?? value)) { + await this.create(relation.table as any, { + ...item, + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), + }, { upsert: true }) + } + } + if (value.$upsert) { + await this.upsert(relation.table as any, makeArray(value.$upsert).map(r => ({ + ...r, + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), + }))) + } + } else if (relation.type === 'manyToOne') { + if (value.$create) { + const result = await this.create(relation.table as any, value.$create, { upsert: true }) + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) + } else if (value.$upsert) { + await this.upsert(relation.table as any, [value.$upsert]) + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) + } else if (value.$connect) { + const result = relation.references.every(k => value.$connect![k] !== undefined) ? value.$connect + : await this.get(relation.table as any, value.$connect as any) + if (result.length !== 1) throw new Error('relation not found') + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) + } + } else if (relation.type === 'manyToMany') { + const assocTable = Relation.buildAssociationTable(relation.table as any, table) + const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) + const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) + const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { + field: x, + reference: y, + }] as const) + const result: any[] = [] + if (value.$create) { + for (const item of makeArray(value.$create)) { + result.push(await this.create(relation.table as any, { + ...item, + ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(item, v.reference) ?? getCell(data, v.field)])), + }, { upsert: true })) + } + } + if (value.$upsert) { + const upsert = makeArray(value.$upsert ?? value.$create).map(r => ({ + ...r, + ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(r, v.reference) ?? getCell(data, v.field)])), + })) + await this.upsert(relation.table as any, upsert) + result.push(...upsert) + } + if (value.$connect) { + for (const item of makeArray(value.$connect)) { + if (references.every(k => item[k] !== undefined)) result.push(item) + else result.push(...await this.get(relation.table as any, item)) + } + } + await this.upsert(assocTable as any, result.map(r => ({ + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(r, v.reference) ?? getCell(data, v.field)])), + ...Object.fromEntries(fields.map((k, i) => [k, getCell(data, relation.fields[i])])), + ...Object.fromEntries(references.map((k, i) => [k, getCell(r, relation.references[i])])), + } as any))) + } } - return sel._action('create', sel.model.create(data)).execute() + return data } async upsert>( @@ -514,20 +601,6 @@ export class Database extends Servi ): Promise { const sel = this.select(table) if (typeof upsert === 'function') upsert = upsert(sel.row) - else { - const tasks = this.transformUpsert(sel, table, upsert, keys) - if (Object.keys(tasks).length > 1) { - return this.ensureTransaction(async (database) => { - for (const [key, { table, upsert, keys }] of Object.entries(tasks)) { - if (!key) continue - await database.upsert(table as any, upsert, keys as any) - } - return database.upsert(table, tasks[''].upsert, keys) - }) - } else { - upsert = tasks[''].upsert - } - } upsert = upsert.map(item => sel.model.format(item)) keys = makeArray(keys || sel.model.primary) as any return sel._action('upsert', upsert, keys).execute() @@ -681,117 +754,99 @@ export class Database extends Servi return { $expr: Eval.and(...results) } as any } - private transformUpsert(sel: Selection, table: string, upsert: any[], keys?: MaybeArray) { - const tasks: Dict<{ - table: string - upsert: any[] - keys: string[] - }> = {} - - const pushTask = (table: string, upsert: any[], keys?: MaybeArray, key?: string) => { - keys = makeArray(keys ?? this.tables[table].primary) - key ??= [table, ...keys].join('_$_') - ;(tasks[key] ??= { - table, - upsert: [], - keys, - }).upsert.push(...upsert) - } - - const upsert2 = upsert.map(data => { - for (const key in data) { - if (data[key] && this.tables[table].fields[key]?.relation) { - const relation = this.tables[table].fields[key].relation - if (relation.type === 'oneToOne' && relation.required) { - const mergedData = { ...data[key] } - for (const k in relation.fields) { - mergedData[relation.references[k]] = data[relation.fields[k]] - } - pushTask(relation.table, [mergedData], relation.references) - } else if (relation.type === 'manyToOne' && data[key].$create) { - pushTask(relation.table, [data[key].$create], relation.references) - data = { - ...omit(data, [key]), - ...Object.fromEntries(Object.entries(data[key].$create) - .filter(([k]) => sel.model.fields[`${key}.${k}`]) - .map(([k, v]) => [`${key}.${k}`, v])), - } - continue - } else if (relation.type === 'oneToMany' && Array.isArray(data[key])) { - const mergedData = data[key].map(row => { - const mergedData = { ...row } - for (const k in relation.fields) { - mergedData[relation.references[k]] = data[relation.fields[k]] - } - return mergedData - }) - pushTask(relation.table, mergedData) - } else if (relation.type === 'manyToMany') { - const assocTable: any = Relation.buildAssociationTable(table, relation.table) - const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) - const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) - pushTask(assocTable, data[key].map(r => ({ - ...Object.fromEntries(fields.map((k, i) => [k, getCell(data, relation.fields[i])])), - ...Object.fromEntries(references.map((k, i) => [k, getCell(r, relation.references[i])])), - }))) - pushTask(relation.table, data[key]) - } else { - // handle shadowed fields - data = { - ...omit(data, [key]), - ...Object.fromEntries(Object.entries(data[key]) - .filter(([k]) => sel.model.fields[`${key}.${k}`]) - .map(([k, v]) => [`${key}.${k}`, v])), - } - continue - } - data = omit(data, [key]) + private async processRelationUpdate(table: any, row: any, key: any, value: Relation.Modifier) { + const model = this.tables[table] + const relation: Relation.Config = this.tables[table].fields[key]!.relation! as any + if (relation.type === 'oneToOne') { + if (value === null) { + await this.remove(relation.table as any, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) + } else if (value.$create) { + const result = await this.create(relation.table, { + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...value.$create, + }, { upsert: true }) + if (!relation.fields.some(x => x in makeArray(model.primary))) { + relation.references.forEach((k, i) => row[relation.fields[i]] = getCell(result, k)) } + } else if (value.$set || typeof value === 'function') { + await this.set( + relation.table as any, + Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + value.$set ?? value as any, + ) + } else { + await this.upsert(relation.table, makeArray(value.$upsert ?? value).map(r => ({ + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...r, + }))) } - return data - }) - pushTask(table, upsert2, keys, '') - Object.values(tasks).forEach(task => { - const result: any[] = [], added = new Set() - task.upsert.map(data => { - const primary = task.keys.map(k => getCell(data, k)) - if (primary.some(isNullable)) { - result.push(data) - } else { - const key = primary.join('_$_') - if (!added.has(key)) { - result.push(data) - added.add(key) - } - } - }) - task.upsert = result - }) - return tasks - } - - private async processRelationUpdate(table: any, row: any, key: any, modifier: Relation.Modifier) { - const relation: Relation.Config = this.tables[table].fields[key]!.relation! as any - if (Array.isArray(modifier)) { - if (relation.type === 'oneToMany') { - modifier = { $remove: {}, $create: modifier } - } else if (relation.type === 'manyToMany') { - throw new Error('override for manyToMany relation is not supported') + } else if (relation.type === 'manyToOne') { + if (value === null) { + // relation.fields.forEach((x, i) => row[x] = null) + await this.set( + table, + pick(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, null])) as any, + ) } - } - if (modifier.$remove) { - if (relation.type === 'oneToMany') { + if (!isEvalExpr(value)) { + value = { $upsert: value } as any + } + if (value.$remove) { + await this.remove(relation.table, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) + } + if (value.$set || typeof value === 'function') { + await this.set( + relation.table as any, + Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + value.$set ?? value as any, + ) + } + if (value.$create) { + // throw new Error('cannot create manyToOne relation in set') + const result = await this.create(relation.table as any, value.$create, { upsert: true }) + await this.set( + table, + omit(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result, relation.references[i])])) as any, + ) + // relation.references.forEach((k, i) => row[relation.fields[i]] = getCell(result, k)) + } + if (value.$upsert) { + await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...r, + }))) + } + if (value.$disconnect) { + await this.set( + table, + pick(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, null])) as any, + ) + } + if (value.$connect) { + const result = await this.get(relation.table as any, value.$connect)?.[0] + if (!result) throw new Error('cannot find relation') + await this.set( + table, + pick(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result, relation.references[i])])) as any, + ) + } + } else if (relation.type === 'oneToMany') { + if (Array.isArray(value)) { + // When should we do upsert + value = { $remove: {}, $create: value } + } + if (value.$remove) { await this.remove(relation.table, (r: any) => Eval.query(r, { ...Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])), - ...(typeof modifier.$remove === 'function' ? { $expr: modifier.$remove(r) } : modifier.$remove), + ...(typeof value.$remove === 'function' ? { $expr: value.$remove(r) } : value.$remove), }) as any) - } else if (relation.type === 'manyToMany') { - throw new Error('remove for manyToMany relation is not supported') } - } - if (modifier.$set) { - if (relation.type === 'oneToMany') { - for (const setexpr of makeArray(modifier.$set) as any[]) { + if (value.$set) { + for (const setexpr of makeArray(value.$set) as any[]) { const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] await this.set(relation.table, (r: any) => Eval.query(r, { @@ -801,63 +856,118 @@ export class Database extends Servi update, ) } - } else if (relation.type === 'manyToMany') { - throw new Error('set for manyToMany relation is not supported') } - } - if (modifier.$create) { - if (relation.type === 'oneToMany') { - const upsert = makeArray(modifier.$create).map((r: any) => { - const data = { ...r } - for (const k in relation.fields) { - data[relation.references[k]] = row[relation.fields[k]] - } - return data - }) - await this.upsert(relation.table, upsert) - } else if (relation.type === 'manyToMany') { - throw new Error('create for manyToMany relation is not supported') + if (value.$create) { + for (const item of makeArray(value.$create)) { + await this.create(relation.table, { + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...item, + }, { upsert: true }) + } } - } - if (modifier.$disconnect) { - if (relation.type === 'oneToMany') { + if (value.$upsert) { + await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...r, + }))) + } + if (value.$disconnect) { await this.set(relation.table, (r: any) => Eval.query(r, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), - ...(typeof modifier.$disconnect === 'function' ? { $expr: modifier.$disconnect } : modifier.$disconnect), + ...(typeof value.$disconnect === 'function' ? { $expr: value.$disconnect } : value.$disconnect), } as any), Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, ) - } else if (relation.type === 'manyToMany') { - const assocTable = Relation.buildAssociationTable(table, relation.table) as Keys - const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) - const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) + } + if (value.$connect) { + await this.set(relation.table, + value.$connect, + Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])) as any, + ) + } + } else { + const assocTable = Relation.buildAssociationTable(table, relation.table) as Keys + const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) + const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) + const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { + field: x, + reference: y, + }] as const) + if (Array.isArray(value)) { + // When should we do upsert + value = { $disconnect: {}, $connect: value } + } + if (value.$remove) { const rows = await this.select(assocTable, { + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), ...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, - [relation.table]: modifier.$disconnect, + [relation.table]: value.$disconnect, }, null).execute() await this.remove(assocTable, r => Eval.in( - [...fields.map(x => r[x]), ...references.map(x => r[x])], - rows.map(r => [...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), + [...shared.map(([k, v]) => r[k]), ...fields.map(x => r[x]), ...references.map(x => r[x])], + rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), + )) + await this.remove(relation.table, (r) => Eval.in( + [...shared.map(([k, v]) => r[v.reference]), ...relation.references.map(x => r[x])], + rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...references.map(x => getCell(r, x))]), )) } - } - if (modifier.$connect) { - if (relation.type === 'oneToMany') { - await this.set(relation.table, - modifier.$connect, - Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])) as any, - ) - } else if (relation.type === 'manyToMany') { - const assocTable: any = Relation.buildAssociationTable(table, relation.table) - const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) - const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) + if (value.$set) { + for (const setexpr of makeArray(value.$set) as any[]) { + const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] + const rows = await this.select(assocTable, { + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), + ...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + [relation.table]: typeof query === 'function' ? { $expr: query } : query, + }, null).execute() + await this.set(relation.table, + (r) => Eval.in( + [...shared.map(([k, v]) => r[v.reference]), ...relation.references.map(x => r[x])], + rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...references.map(x => getCell(r, x))]), + ), + update, + ) + } + } + if (value.$create) { + const result: any[] = [] + for (const item of makeArray(value.$create)) { + result.push(await this.create(relation.table, { + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...item, + }, { upsert: true })) + } + await this.upsert(assocTable, result.map(r => ({ + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), + ...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])), + ...Object.fromEntries(references.map((k, i) => [k, r[relation.references[i] as any]])), + })) as any) + } + if (value.$upsert) { + await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...r, + }))) + } + if (value.$disconnect) { + const rows = await this.select(assocTable, { + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), + ...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + [relation.table]: value.$disconnect, + }, null).execute() + await this.remove(assocTable, r => Eval.in( + [...shared.map(([k, v]) => r[k]), ...fields.map(x => r[x]), ...references.map(x => r[x])], + rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), + )) + } + if (value.$connect) { const rows = await this.get(relation.table, (r: any) => Eval.query(r, { - ...Object.fromEntries(Object.entries(relation.shared).map(([k, v]) => [v, getCell(row, k)])), - ...(typeof modifier.$connect === 'function' ? { $expr: modifier.$connect(r) } : modifier.$connect), + ...Object.fromEntries(shared.map(([k, v]) => [v.reference, getCell(row, v.field)])), + ...(typeof value.$connect === 'function' ? { $expr: value.$connect(r) } : value.$connect), }) as any) await this.upsert(assocTable, rows.map(r => ({ - ...mapValues(relation.shared, (v, k) => getCell(row, k)), + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), ...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])), ...Object.fromEntries(references.map((k, i) => [k, r[relation.references[i] as any]])), })) as any) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 0b56746c..31edc83d 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -28,6 +28,11 @@ export namespace Driver { include?: Relation.Include } + export interface CreateOptions { + upsert?: boolean + include?: any + } + export interface WriteResult { inserted?: number matched?: number diff --git a/packages/core/src/eval.ts b/packages/core/src/eval.ts index 377e2c15..81ec53db 100644 --- a/packages/core/src/eval.ts +++ b/packages/core/src/eval.ts @@ -1,5 +1,5 @@ import { defineProperty, isNullable, mapValues } from 'cosmokit' -import { AtomicTypes, Comparable, DeepPartial, Flatten, isComparable, isEmpty, makeRegExp, Row, Values } from './utils.ts' +import { AtomicTypes, Comparable, Flatten, isComparable, isEmpty, makeRegExp, Row, Values } from './utils.ts' import { Type } from './type.ts' import { Field, Relation } from './model.ts' import { Query } from './query.ts' @@ -31,7 +31,7 @@ type UnevalObject = { export type Uneval = | U extends Values ? Eval.Term : U extends (infer T extends object)[] ? Relation.Modifier | Eval.Array - : U extends object ? Eval.Expr | UnevalObject> | { $create: DeepPartial } + : U extends object ? Eval.Expr | UnevalObject> | Relation.Modifier : any export type Eval = diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index b474d826..47c4d511 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,11 +1,12 @@ import { clone, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit' import { Context } from 'cordis' import { Eval, Update } from './eval.ts' -import { DeepPartial, FlatKeys, Flatten, isFlat, Keys, Row, unravel } from './utils.ts' +import { DeepPartial, FlatKeys, Flatten, isFlat, Keys, Row, unravel, Values } from './utils.ts' import { Type } from './type.ts' import { Driver } from './driver.ts' import { Query } from './query.ts' import { Selection } from './selection.ts' +import { Create } from './database.ts' const Primary = Symbol('minato.primary') export type Primary = (string | number) & { [Primary]: true } @@ -23,7 +24,6 @@ export namespace Relation { references: Keys[] fields: K[] shared: Record> - mixin?: K[] required: boolean } @@ -36,21 +36,35 @@ export namespace Relation { shared?: MaybeArray | Record } + export interface Options = Keys> { + many: boolean + table: K + field?: Keys> | Keys[]> + ref?: MaybeArray> + nullable?: boolean + } + + export interface Extra { + shared?: string[] + extra?: object + } + export type Include = boolean | { [P in keyof T]?: T[P] extends MaybeArray | undefined ? U extends S ? Include : never : never } - export type SetExpr = Row.Computed> | { + export type SetExpr = ((row: Row) => Update) | { where: Query.Expr> | Selection.Callback update: Row.Computed> } - export interface Modifier { - $create?: MaybeArray> - $set?: MaybeArray> - $remove?: Query.Expr> | Selection.Callback - $connect?: Query.Expr> | Selection.Callback - $disconnect?: Query.Expr> | Selection.Callback + export interface Modifier { + $create?: MaybeArray> + $upsert?: DeepPartial[] + $set?: MaybeArray> + $remove?: Query.Expr> | Selection.Callback + $connect?: Query.Expr> | Selection.Callback + $disconnect?: Query.Expr> | Selection.Callback } export function buildAssociationTable(...tables: [string, string]) { @@ -61,7 +75,8 @@ export namespace Relation { return `${table}.${key}` } - export function transformAssoicationFields(fields: string[], table: string) { + export function buildSharedKey(field: string, reference: string) { + return [field, reference].sort().join('_') } export function parse(def: Definition, key: string, model: Model, relmodel: Model): [Config, Config] { @@ -76,11 +91,12 @@ export namespace Relation { type: def.type, table: def.table ?? relmodel.name, fields: makeArray(fields), - shared: mapValues(shared, (_, k) => k), + shared, references: makeArray(def.references ?? relmodel.primary), required: def.type !== 'manyToOne' && model.name !== relmodel.name && makeArray(fields).every(key => !model.fields[key]?.nullable || makeArray(model.primary).includes(key)), } + // remove shared keys from fields and references Object.entries(shared).forEach(([k, v]) => { relation.fields = relation.fields.filter(x => x !== k) relation.references = relation.references.filter(x => x !== v) @@ -92,7 +108,7 @@ export namespace Relation { table: model.name, fields: relation.references, references: relation.fields, - shared, + shared: Object.fromEntries(Object.entries(shared).map(([k, v]) => [v, k])), required: relation.type !== 'oneToMany' && !relation.required && relation.references.every(key => !relmodel.fields[key]?.nullable || makeArray(relmodel.primary).includes(key)), } diff --git a/packages/tests/src/index.ts b/packages/tests/src/index.ts index b7c79152..c5425317 100644 --- a/packages/tests/src/index.ts +++ b/packages/tests/src/index.ts @@ -63,14 +63,14 @@ function createUnit(target: T, root = false): Unit { } namespace Tests { - // export const model = ModelOperations - // export const query = QueryOperators - // export const update = UpdateOperators - // export const object = ObjectOperations - // export const selection = Selection - // export const migration = Migration - // export const json = Json - // export const transaction = Transaction + export const model = ModelOperations + export const query = QueryOperators + export const update = UpdateOperators + export const object = ObjectOperations + export const selection = Selection + export const migration = Migration + export const json = Json + export const transaction = Transaction export const relation = Relation } diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index e3220c92..f5b1694c 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -39,22 +39,34 @@ interface Post2Tag { tag?: Tag } -interface GuildSync { - syncAt?: Date + +interface GuildSyncRef { + syncAt: number } -interface Login extends GuildSync { +interface Login extends GuildSyncRef { id: string platform: string name?: string guilds?: Guild[] + syncs?: GuildSync[] } -interface Guild extends GuildSync { +interface Guild extends GuildSyncRef { id: string platform2: string name?: string logins?: Login[] + syncs?: GuildSync[] +} + +// interface GuildSync extends Login { } +// interface GuildSync extends Guild { } + +interface GuildSync { + syncAt?: number + guild?: Guild + login?: Login } interface Tables { @@ -63,7 +75,7 @@ interface Tables { post: Post tag: Tag post2tag: Post2Tag - // guildSync: GuildSync + guildSync: GuildSync login: Login guild: Guild } @@ -95,26 +107,37 @@ function RelationTests(database: Database) { id: 'unsigned', score: 'unsigned', content: 'string', - extraFields: { - type: 'expr', - nullable: true, - }, - author: { - type: 'manyToOne', - table: 'user', - target: 'posts', - }, + // extraFields: { + // type: 'expr', + // nullable: true, + // }, + // author: { + // type: 'manyToOne', + // table: 'user', + // target: 'posts', + // }, }, { autoInc: true, }) + database.relate({ + many: false, + table: 'user', + field: 'posts', + }, { + many: true, + table: 'post', + field: 'author', + nullable: true, + }) + database.extend('tag', { id: 'unsigned', name: 'string', - extraFields: { - type: 'expr', - nullable: true, - }, + // extraFields: { + // type: 'expr', + // nullable: true, + // }, posts: { type: 'manyToMany', table: 'post', @@ -164,6 +187,45 @@ function RelationTests(database: Database) { primary: ['id', 'platform2'] }) + // database.relate({ + // many: true, + // table: 'guild', + // field: 'logins', + // }, { + // many: true, + // table: 'login', + // field: 'guilds', + // nullable: true, + // }, { + // shared: { + // platform2: 'platform' + // }, + // extra: { + // syncAt: 'unsigned', + // } + // }) + + database.extend('guildSync', { + syncAt: 'unsigned', + platform: 'string', + "guild.id": 'string', + "login.id": 'string', + guild: { + type: 'manyToOne', + table: 'guild', + target: 'syncs', + fields: ['guild.id', 'platform'], + }, + login: { + type: 'manyToOne', + table: 'login', + target: 'syncs', + fields: ['login.id', 'platform'], + } + }, { + primary: ['platform', 'guild.id', 'login.id'] + }) + async function setupAutoInc(database: Database, name: K, length: number) { await database.upsert(name, Array(length).fill({})) await database.remove(name, {}) @@ -304,802 +366,917 @@ namespace RelationTests { // explicit manyToMany await expect(database.select('post', {}, { _tags: { tag: { _posts: { post: true } } } }).execute()).to.eventually.be.fulfilled - // await expect(database.select('post', {}, { tags: { posts: true } }).execute()).to.eventually.have.shape( - // posts.map(post => ({ - // ...post, - // tags: post2tags.filter(p2t => p2t.post?.id === post.id) - // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) - // .filter(tag => tag) - // .map(tag => ({ - // ...tag, - // posts: post2tags.filter(p2t => p2t.tag?.id === tag!.id).map(p2t => posts.find(post => post.id === p2t.post?.id)), - // })), - // })), - // ) - - console.dir((await database.select('post', {}, { tags: true }).execute()), { depth: 10}) + await expect(database.select('post', {}, { tags: { posts: true } }).execute()).to.eventually.have.shape( + posts.map(post => ({ + ...post, + tags: post2tags.filter(p2t => p2t.post?.id === post.id) + .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + .filter(tag => tag) + .map(tag => ({ + ...tag, + posts: post2tags.filter(p2t => p2t.tag?.id === tag!.id).map(p2t => posts.find(post => post.id === p2t.post?.id)), + })), + })), + ) }) } - // export function query(database: Database) { - // it('oneToOne / manyToOne', async () => { - // const users = await setup(database, 'user', userTable) - // const profiles = await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) + export function query(database: Database) { + it('oneToOne / manyToOne', async () => { + const users = await setup(database, 'user', userTable) + const profiles = await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) - // await expect(database.get('user', { - // profile: { - // user: { - // id: 1, - // }, - // }, - // })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ - // ...user, - // profile: profiles.find(profile => profile.id === user.id), - // }))) - - // await expect(database.get('user', row => $.query(row, { - // profile: r => $.eq(r.id, row.id), - // }))).to.eventually.have.shape(users.map(user => ({ - // ...user, - // profile: profiles.find(profile => profile.id === user.id), - // }))) - - // await expect(database.get('user', { - // profile: { - // user: { - // value: 1, - // }, - // }, - // })).to.eventually.have.shape(users.slice(1, 2).map(user => ({ - // ...user, - // profile: profiles.find(profile => profile.id === user.id), - // }))) - - // await expect(database.get('post', { - // author: { - // id: 1, - // }, - // })).to.eventually.have.shape(posts.map(post => ({ - // ...post, - // author: users.find(user => post.author?.id === user.id), - // })).filter(post => post.author?.id === 1)) - - // await expect(database.get('post', { - // author: { - // id: 1, - // value: 1, - // }, - // })).to.eventually.have.length(0) - // }) - - // it('oneToMany', async () => { - // const users = await setup(database, 'user', userTable) - // const profiles = await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) - - // await expect(database.get('user', { - // posts: { - // $some: { - // author: { - // id: 1, - // }, - // }, - // }, - // })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ - // ...user, - // posts: posts.filter(post => post.author?.id === user.id), - // }))) - - // await expect(database.get('user', { - // posts: { - // $some: row => $.eq(row.id, 1), - // }, - // })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ - // ...user, - // posts: posts.filter(post => post.author?.id === user.id), - // }))) - - // await expect(database.get('user', { - // posts: { - // $none: { - // author: { - // id: 1, - // }, - // }, - // }, - // })).to.eventually.have.shape(users.slice(1).map(user => ({ - // ...user, - // posts: posts.filter(post => post.author?.id === user.id), - // }))) - - // await expect(database.get('user', { - // posts: { - // $every: { - // author: { - // id: 1, - // }, - // }, - // }, - // })).to.eventually.have.shape([users[0], users[2]].map(user => ({ - // ...user, - // posts: posts.filter(post => post.author?.id === user.id), - // }))) - // }) - - // it('manyToMany', async () => { - // const users = await setup(database, 'user', userTable) - // const profiles = await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) - // const tags = await setup(database, 'tag', tagTable) - // const post2tags = await setup(database, 'post2tag', post2TagTable) - // const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) - - // await expect(database.get('post', { - // tags: { - // $some: { - // id: 1, - // }, - // }, - // })).to.eventually.have.shape(posts.slice(0, 2).map(post => ({ - // ...post, - // tags: post2tags.filter(p2t => p2t.post?.id === post.id) - // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) - // .filter(tag => tag), - // }))) - - // await expect(database.get('post', { - // tags: { - // $none: { - // id: 1, - // }, - // }, - // })).to.eventually.have.shape(posts.slice(2).map(post => ({ - // ...post, - // tags: post2tags.filter(p2t => p2t.post?.id === post.id) - // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) - // .filter(tag => tag), - // }))) - - // await expect(database.get('post', { - // tags: { - // $every: { - // id: 3, - // }, - // }, - // })).to.eventually.have.shape(posts.slice(2, 3).map(post => ({ - // ...post, - // tags: post2tags.filter(p2t => p2t.post?.id === post.id) - // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) - // .filter(tag => tag), - // }))) - - // await expect(database.get('post', { - // tags: { - // $some: 1, - // $none: [3], - // $every: {}, - // }, - // })).to.eventually.have.shape(posts.slice(0, 1).map(post => ({ - // ...post, - // tags: post2tags.filter(p2t => p2t.post?.id === post.id) - // .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) - // .filter(tag => tag), - // }))) - // }) - - // it('nested query', async () => { - // const users = await setup(database, 'user', userTable) - // const profiles = await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) - // const tags = await setup(database, 'tag', tagTable) - // const post2tags = await setup(database, 'post2tag', post2TagTable) - // const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) - - // await expect(database.get('user', { - // posts: { - // $some: { - // tags: { - // $some: { - // id: 1, - // }, - // }, - // }, - // }, - // })).to.eventually.have.shape([users[0]].map(user => ({ - // ...user, - // posts: posts.filter(post => post.author?.id === user.id), - // }))) - - // await expect(database.get('tag', { - // posts: { - // $some: { - // author: { - // id: 2, - // }, - // }, - // }, - // })).to.eventually.have.shape([tags[2]].map(tag => ({ - // ...tag, - // posts: post2tags.filter(p2t => p2t.tag?.id === tag.id) - // .map(p2t => posts.find(post => post.id === p2t.post?.id)) - // .filter(post => post), - // }))) - // }) - - // it('omit query', async () => { - // const users = await setup(database, 'user', userTable) - // const profiles = await setup(database, 'profile', profileTable) - - // await expect(database.get('user', { id: 2 }, ['id', 'profile'])).to.eventually.have.shape( - // [users[1]].map(user => ({ - // id: user.id, - // profile: profiles.find(profile => user.id === profile.id), - // })), - // ) - // }) - - // it('existence', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'profile', profileTable) - // await setup(database, 'post', postTable) - - // await expect(database.select('user', { successor: null }, null).execute()).to.eventually.have.shape([ - // { id: 1 }, - // { id: 3 }, - // ]) - - // await expect(database.select('user', { predecessor: null }, null).execute()).to.eventually.have.shape([ - // { id: 2 }, - // { id: 3 }, - // ]) - - // await database.set('user', 1, { profile: null }) - // await expect(database.select('user', { profile: null }, null).execute()).to.eventually.have.shape([ - // { id: 1 }, - // ]) - - // await database.set('user', 2, { - // posts: { - // $disconnect: { - // id: 3, - // }, - // }, - // }) - // await expect(database.select('post', { author: null }, null).execute()).to.eventually.have.shape([ - // { id: 3 }, - // ]) - // await expect(database.select('user', { - // posts: { - // $every: { - // author: null, - // }, - // }, - // }, null).execute()).to.eventually.have.shape([ - // { id: 2 }, - // { id: 3 }, - // ]) - // }) - - // it('manyToOne fallbacck', async () => { - // await setup(database, 'user', []) - // await setup(database, 'profile', []) - // await setup(database, 'post', []) - - // await database.create('post', { - // id: 1, - // content: 'new post', - // author: { - // id: 2, - // } - // }) + await expect(database.get('user', { + profile: { + user: { + id: 1, + }, + }, + })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ + ...user, + profile: profiles.find(profile => profile.id === user.id), + }))) + + await expect(database.get('user', row => $.query(row, { + profile: r => $.eq(r.id, row.id), + }))).to.eventually.have.shape(users.map(user => ({ + ...user, + profile: profiles.find(profile => profile.id === user.id), + }))) + + await expect(database.get('user', { + profile: { + user: { + value: 1, + }, + }, + })).to.eventually.have.shape(users.slice(1, 2).map(user => ({ + ...user, + profile: profiles.find(profile => profile.id === user.id), + }))) + + await expect(database.get('post', { + author: { + id: 1, + }, + })).to.eventually.have.shape(posts.map(post => ({ + ...post, + author: users.find(user => post.author?.id === user.id), + })).filter(post => post.author?.id === 1)) + + await expect(database.get('post', { + author: { + id: 1, + value: 1, + }, + })).to.eventually.have.length(0) + }) - // await expect(database.get('post', 1, ['author'])).to.eventually.have.shape([{ - // author: { - // id: 2, - // }, - // }]) + it('oneToMany', async () => { + const users = await setup(database, 'user', userTable) + const profiles = await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) - // await database.create('user', { - // id: 2, - // value: 123, - // }) + await expect(database.get('user', { + posts: { + $some: { + author: { + id: 1, + }, + }, + }, + })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ + ...user, + posts: posts.filter(post => post.author?.id === user.id), + }))) + + await expect(database.get('user', { + posts: { + $some: row => $.eq(row.id, 1), + }, + })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ + ...user, + posts: posts.filter(post => post.author?.id === user.id), + }))) + + await expect(database.get('user', { + posts: { + $none: { + author: { + id: 1, + }, + }, + }, + })).to.eventually.have.shape(users.slice(1).map(user => ({ + ...user, + posts: posts.filter(post => post.author?.id === user.id), + }))) + + await expect(database.get('user', { + posts: { + $every: { + author: { + id: 1, + }, + }, + }, + })).to.eventually.have.shape([users[0], users[2]].map(user => ({ + ...user, + posts: posts.filter(post => post.author?.id === user.id), + }))) + }) - // await expect(database.get('post', 1, ['author'])).to.eventually.have.shape([{ - // author: { - // id: 2, - // value: 123, - // }, - // }]) - // }) - // } - - // export function create(database: Database) { - // it('basic support', async () => { - // await setup(database, 'user', []) - // await setup(database, 'profile', []) - // await setup(database, 'post', []) - - // for (const user of userTable) { - // await expect(database.create('user', { - // ...user, - // profile: { - // ...profileTable.find(profile => profile.id === user.id)!, - // }, - // posts: postTable.filter(post => post.author?.id === user.id), - // })).to.eventually.have.shape(user) - // } - - // await expect(database.select('profile', {}, { user: true }).execute()).to.eventually.have.shape( - // profileTable.map(profile => ({ - // ...profile, - // user: userTable.find(user => user.id === profile.id), - // })), - // ) - - // await expect(database.select('user', {}, { profile: true, posts: true }).execute()).to.eventually.have.shape( - // userTable.map(user => ({ - // ...user, - // profile: profileTable.find(profile => profile.id === user.id), - // posts: postTable.filter(post => post.author?.id === user.id), - // })), - // ) - // }) - - // it('oneToMany', async () => { - // await setup(database, 'user', []) - // await setup(database, 'profile', []) - // await setup(database, 'post', []) - - // for (const user of userTable) { - // await database.create('user', { - // ...userTable.find(u => u.id === user.id)!, - // posts: postTable.filter(post => post.author?.id === user.id), - // }) - // } - - // await expect(database.select('user', {}, { posts: true }).execute()).to.eventually.have.shape( - // userTable.map(user => ({ - // ...user, - // posts: postTable.filter(post => post.author?.id === user.id), - // })), - // ) - // }) - - // it('manyToOne', async () => { - // const users = await setup(database, 'user', []) - // await setup(database, 'post', []) - - // users.push({ id: 1, value: 2 }) - - // await database.create('post', { - // id: 1, - // content: 'post2', - // author: { - // $create: { - // id: 1, - // value: 2, - // }, - // }, - // }) - // await expect(database.get('user', {})).to.eventually.have.shape(users) - - // users[0].value = 3 - // await database.create('post', { - // id: 2, - // content: 'post3', - // author: { - // $create: { - // id: 1, - // value: 3, - // }, - // }, - // }) - // await expect(database.get('user', {})).to.eventually.have.shape(users) - - // await database.create('post', { - // id: 3, - // content: 'post4', - // author: { - // id: 1, - // value: 30, - // }, - // }) - // await expect(database.get('user', {})).to.eventually.have.shape(users) - // await expect(database.get('post', {}, { include: { author: true } })).to.eventually.have.shape([ - // { id: 1, content: 'post2', author: { id: 1, value: 3 } }, - // { id: 2, content: 'post3', author: { id: 1, value: 3 } }, - // { id: 3, content: 'post4', author: { id: 1, value: 3 } }, - // ]) - // }) - - - // it('upsert manyToOne', async () => { - // const users = await setup(database, 'user', []) - // await setup(database, 'post', []) - - // users.push({ id: 1, value: 2 }) - - // await database.upsert('post', [{ - // id: 1, - // content: 'post2', - // author: { - // $create: { - // id: 1, - // value: 2, - // }, - // }, - // }]) - // await expect(database.get('user', {})).to.eventually.have.shape(users) - - // users[0].value = 3 - // await database.upsert('post', [{ - // id: 2, - // content: 'post3', - // author: { - // $create: { - // id: 1, - // value: 3, - // }, - // }, - // }, - // { - // id: 3, - // content: 'post4', - // author: { - // id: 1, - // value: 30, - // }, - // }]) - // await expect(database.get('user', {})).to.eventually.have.shape(users) - // await expect(database.get('post', {}, { include: { author: true } })).to.eventually.have.shape([ - // { id: 1, content: 'post2', author: { id: 1, value: 3 } }, - // { id: 2, content: 'post3', author: { id: 1, value: 3 } }, - // { id: 3, content: 'post4', author: { id: 1, value: 3 } }, - // ]) - // }) - - // it('manyToMany', async () => { - // await setup(database, 'user', []) - // await setup(database, 'post', []) - // await setup(database, 'tag', []) - - // for (const user of userTable) { - // await database.create('user', { - // ...userTable.find(u => u.id === user.id)!, - // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ - // ...post, - // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), - // })), - // }) - // } - - // await expect(database.select('user', {}, { posts: { tags: true } }).execute()).to.eventually.have.shape( - // userTable.map(user => ({ - // ...user, - // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ - // ...post, - // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)), - // })), - // })), - // ) - // }) - - // it('upsert manyToMany', async () => { - // await setup(database, 'user', []) - // await setup(database, 'post', []) - // await setup(database, 'tag', []) - - // await database.upsert('user', userTable.map(user => ({ - // ...userTable.find(u => u.id === user.id)!, - // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ - // ...post, - // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), - // })), - // }))) - - // await expect(database.select('user', {}, { posts: { tags: true } }).execute()).to.eventually.have.shape( - // userTable.map(user => ({ - // ...user, - // posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ - // ...post, - // tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)), - // })), - // })), - // ) - // }) - // } - - // export function modify(database: Database, options: RelationOptions = {}) { - // const { ignoreNullObject = true } = options - - // it('oneToOne / manyToOne', async () => { - // const users = await setup(database, 'user', userTable) - // const profiles = await setup(database, 'profile', profileTable) - // await setup(database, 'post', postTable) - - // profiles.splice(2, 1) - // await database.set('user', 3, { - // profile: null, - // }) - // await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + it('manyToMany', async () => { + const users = await setup(database, 'user', userTable) + const profiles = await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) + const tags = await setup(database, 'tag', tagTable) + const post2tags = await setup(database, 'post2tag', post2TagTable) + const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) - // profiles.push(database.tables['profile'].create({ id: 3, name: 'Reborn' })) - // await database.set('user', 3, { - // profile: { - // name: 'Reborn', - // }, - // }) - // await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + await expect(database.get('post', { + tags: { + $some: { + id: 1, + }, + }, + })).to.eventually.have.shape(posts.slice(0, 2).map(post => ({ + ...post, + tags: post2tags.filter(p2t => p2t.post?.id === post.id) + .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + .filter(tag => tag), + }))) + + await expect(database.get('post', { + tags: { + $none: { + id: 1, + }, + }, + })).to.eventually.have.shape(posts.slice(2).map(post => ({ + ...post, + tags: post2tags.filter(p2t => p2t.post?.id === post.id) + .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + .filter(tag => tag), + }))) + + await expect(database.get('post', { + tags: { + $every: { + id: 3, + }, + }, + })).to.eventually.have.shape(posts.slice(2, 3).map(post => ({ + ...post, + tags: post2tags.filter(p2t => p2t.post?.id === post.id) + .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + .filter(tag => tag), + }))) + + await expect(database.get('post', { + tags: { + $some: 1, + $none: [3], + $every: {}, + }, + })).to.eventually.have.shape(posts.slice(0, 1).map(post => ({ + ...post, + tags: post2tags.filter(p2t => p2t.post?.id === post.id) + .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) + .filter(tag => tag), + }))) + }) - // users[0].value = 99 - // await database.set('post', 1, { - // author: { - // value: 99, - // }, - // }) - // await expect(database.get('user', {})).to.eventually.have.deep.members(users) + it('nested query', async () => { + const users = await setup(database, 'user', userTable) + const profiles = await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) + const tags = await setup(database, 'tag', tagTable) + const post2tags = await setup(database, 'post2tag', post2TagTable) + const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) - // profiles.splice(2, 1) - // await database.set('user', 3, { - // profile: null, - // }) - // await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) - - // users.push({ id: 100, value: 200, successor: { id: undefined } } as any) - // await database.set('post', 1, { - // author: { - // id: 100, - // value: 200, - // }, - // }) - // await expect(database.get('user', {})).to.eventually.have.deep.members(users) - // }) - - // it('upsert', async () => { - // await setup(database, 'user', []) - // await setup(database, 'profile', []) - // await setup(database, 'post', []) - - // for (const user of userTable) { - // await database.upsert('user', [{ - // ...userTable.find(u => u.id === user.id)!, - // profile: profileTable.find(profile => profile.id === user.id), - // }] as any) - // } - - // await expect(database.select('user', {}, { profile: true }).execute()).to.eventually.have.shape( - // userTable.map(user => ({ - // ...user, - // profile: profileTable.find(profile => profile.id === user.id), - // })), - // ) - // }) - - // it('create oneToMany', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) - - // posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post1' })) - // posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post2' })) - - // await database.set('user', 2, { - // posts: { - // $create: [ - // { content: 'post1' }, - // { content: 'post2' }, - // ], - // }, - // }) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - - // posts.push(database.tables['post'].create({ id: 101, author: { id: 1 }, content: 'post101' })) - // await database.set('user', 1, row => ({ - // value: $.add(row.id, 98), - // posts: { - // $create: { id: 101, content: 'post101' }, - // }, - // })) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - // }) - - // it('set oneToMany', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) - - // posts[0].score = 2 - // posts[1].score = 3 - // await database.set('user', 1, row => ({ - // posts: { - // $set: r => ({ - // score: $.add(row.id, r.id), - // }), - // }, - // })) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - - // posts[1].score = 13 - // await database.set('user', 1, row => ({ - // posts: { - // $set: { - // where: { score: { $gt: 2 } }, - // update: r => ({ score: $.add(r.score, 10) }), + await expect(database.get('user', { + posts: { + $some: { + tags: { + $some: { + id: 1, + }, + }, + }, + }, + })).to.eventually.have.shape([users[0]].map(user => ({ + ...user, + posts: posts.filter(post => post.author?.id === user.id), + }))) + + await expect(database.get('tag', { + posts: { + $some: { + author: { + id: 2, + }, + }, + }, + })).to.eventually.have.shape([tags[2]].map(tag => ({ + ...tag, + posts: post2tags.filter(p2t => p2t.tag?.id === tag.id) + .map(p2t => posts.find(post => post.id === p2t.post?.id)) + .filter(post => post), + }))) + }) + + it('omit query', async () => { + const users = await setup(database, 'user', userTable) + const profiles = await setup(database, 'profile', profileTable) + + await expect(database.get('user', { id: 2 }, ['id', 'profile'])).to.eventually.have.shape( + [users[1]].map(user => ({ + id: user.id, + profile: profiles.find(profile => user.id === profile.id), + })), + ) + }) + + it('existence', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + await setup(database, 'post', postTable) + + await expect(database.select('user', { successor: null }, null).execute()).to.eventually.have.shape([ + { id: 1 }, + { id: 3 }, + ]) + + await expect(database.select('user', { predecessor: null }, null).execute()).to.eventually.have.shape([ + { id: 2 }, + { id: 3 }, + ]) + + await database.set('user', 1, { profile: null }) + await expect(database.select('user', { profile: null }, null).execute()).to.eventually.have.shape([ + { id: 1 }, + ]) + + await database.set('user', 2, { + posts: { + $disconnect: { + id: 3, + }, + }, + }) + await expect(database.select('post', { author: null }, null).execute()).to.eventually.have.shape([ + { id: 3 }, + ]) + await expect(database.select('user', { + posts: { + $every: { + author: null, + }, + }, + }, null).execute()).to.eventually.have.shape([ + { id: 2 }, + { id: 3 }, + ]) + }) + + it('manyToOne fallbacck', async () => { + await setup(database, 'user', []) + await setup(database, 'profile', []) + await setup(database, 'post', []) + + await database.create('post', { + id: 1, + content: 'new post', + author: { + id: 2, + } + }) + + await expect(database.get('post', 1, ['author'])).to.eventually.have.shape([{ + author: { + id: 2, + }, + }]) + + await database.create('user', { + id: 2, + value: 123, + }) + + await expect(database.get('post', 1, ['author'])).to.eventually.have.shape([{ + author: { + id: 2, + value: 123, + }, + }]) + }) + } + + export function create(database: Database) { + it('basic support', async () => { + await setup(database, 'user', []) + await setup(database, 'profile', []) + await setup(database, 'post', []) + + for (const user of userTable) { + await expect(database.create('user', { + ...user, + profile: { + ...profileTable.find(profile => profile.id === user.id)!, + }, + posts: postTable.filter(post => post.author?.id === user.id), + })).to.eventually.have.shape(user) + } + + await expect(database.select('profile', {}, { user: true }).execute()).to.eventually.have.shape( + profileTable.map(profile => ({ + ...profile, + user: userTable.find(user => user.id === profile.id), + })), + ) + + await expect(database.select('user', {}, { profile: true, posts: true }).execute()).to.eventually.have.shape( + userTable.map(user => ({ + ...user, + profile: profileTable.find(profile => profile.id === user.id), + posts: postTable.filter(post => post.author?.id === user.id), + })), + ) + }) + + it('oneToMany', async () => { + await setup(database, 'user', []) + await setup(database, 'profile', []) + await setup(database, 'post', []) + + for (const user of userTable) { + await database.create('user', { + ...userTable.find(u => u.id === user.id)!, + posts: postTable.filter(post => post.author?.id === user.id), + }) + } + + await expect(database.select('user', {}, { posts: true }).execute()).to.eventually.have.shape( + userTable.map(user => ({ + ...user, + posts: postTable.filter(post => post.author?.id === user.id), + })), + ) + }) + + it('manyToOne', async () => { + const users = await setup(database, 'user', []) + await setup(database, 'post', []) + + users.push({ id: 1, value: 2 }) + + await database.create('post', { + id: 1, + content: 'post2', + author: { + $create: { + id: 1, + value: 2, + }, + }, + }) + await expect(database.get('user', {})).to.eventually.have.shape(users) + + users[0].value = 3 + await database.create('post', { + id: 2, + content: 'post3', + author: { + $create: { + id: 1, + value: 3, + }, + }, + }) + await expect(database.get('user', {})).to.eventually.have.shape(users) + + await database.create('post', { + id: 3, + content: 'post4', + author: { + id: 1, + }, + }) + await expect(database.get('user', {})).to.eventually.have.shape(users) + await expect(database.get('post', {}, { include: { author: true } })).to.eventually.have.shape([ + { id: 1, content: 'post2', author: { id: 1, value: 3 } }, + { id: 2, content: 'post3', author: { id: 1, value: 3 } }, + { id: 3, content: 'post4', author: { id: 1, value: 3 } }, + ]) + }) + + it('manyToMany', async () => { + await setup(database, 'user', []) + await setup(database, 'post', []) + await setup(database, 'tag', []) + + for (const user of userTable) { + await database.create('user', { + ...userTable.find(u => u.id === user.id)!, + posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ + ...post, + tags: { + $upsert: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), + } + })), + }) + } + + await expect(database.select('user', {}, { posts: { tags: true } }).execute()).to.eventually.have.shape( + userTable.map(user => ({ + ...user, + posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ + ...post, + tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)), + })), + })), + ) + }) + + it('explicit manyToMany', async () => { + await setup(database, 'login', []) + await setup(database, 'guild', [ + { id: '1', platform2: 'sandbox', name: 'Guild1' }, + { id: '2', platform2: 'sandbox', name: 'Guild2' }, + { id: '3', platform2: 'sandbox', name: 'Guild3' }, + ]) + await setup(database, Relation.buildAssociationTable('login', 'guild') as any, []) + + await database.create('login', { + id: '1', + platform: 'sandbox', + name: 'Bot1', + syncs: { + $create: [ + { + syncAt: 123, + guild: { + $connect: { id: '1' }, + } + }, + ], + }, + }) + + await expect(database.get('login', { + id: '1', + platform: 'sandbox', + }, { + include: { syncs: { guild: true } }, + })).to.eventually.have.nested.property('[0].syncs').with.length(1) + + }) + } + + export function modify(database: Database, options: RelationOptions = {}) { + const { ignoreNullObject = true } = options + + it('oneToOne / manyToOne', async () => { + const users = await setup(database, 'user', userTable) + const profiles = await setup(database, 'profile', profileTable) + await setup(database, 'post', postTable) + + profiles.splice(2, 1) + await database.set('user', 3, { + profile: null, + }) + await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + + profiles.push(database.tables['profile'].create({ id: 3, name: 'Reborn' })) + await database.set('user', 3, { + profile: { + name: 'Reborn', + }, + }) + await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + + users[0].value = 99 + await database.set('post', 1, { + author: { + value: 99, + }, + }) + await expect(database.get('user', {})).to.eventually.have.deep.members(users) + + profiles.splice(2, 1) + await database.set('user', 3, { + profile: null, + }) + await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + + users.push({ id: 100, value: 200, successor: { id: undefined } } as any) + await database.set('post', 1, { + author: { + id: 100, + value: 200, + }, + }) + await expect(database.get('user', {})).to.eventually.have.deep.members(users) + }) + + it('oneToOne expr', async () => { + await setup(database, 'user', []) + await setup(database, 'profile', []) + await setup(database, 'post', []) + + await database.create('user', { + id: 1, + value: 0, + }) + + await database.set('user', 1, { + profile: { + $create: { + name: 'Apple', + }, + }, + }) + await expect(database.select('user', {}, { profile: true }).execute()).to.eventually.have.shape( + [{ id: 1, value: 0, profile: { name: 'Apple' } }], + ) + + await database.set('user', 1, { + profile: { + $upsert: [{ + name: 'Apple2', + }], + }, + }) + await expect(database.select('user', {}, { profile: true }).execute()).to.eventually.have.shape( + [{ id: 1, value: 0, profile: { name: 'Apple2' } }], + ) + + await database.set('user', 1, { + profile: { + $set: r => ({ + name: $.concat(r.name, '3') + }), + }, + }) + await expect(database.select('user', {}, { profile: true }).execute()).to.eventually.have.shape( + [{ id: 1, value: 0, profile: { name: 'Apple23' } }], + ) + }) + + + ignoreNullObject && it('manyToOne expr', async () => { + await setup(database, 'user', []) + await setup(database, 'profile', []) + await setup(database, 'post', []) + + await database.create('post', { + id: 1, + content: 'Post1', + }) + + await database.set('post', 1, { + author: { + $create: { + id: 1, + value: 0, + profile: { + $create: { + name: 'Apple', + }, + }, + } + } + }) + await expect(database.select('user', {}, { posts: true, profile: true }).execute()).to.eventually.have.shape( + [{ id: 1, value: 0, profile: { name: 'Apple' }, posts: [{ id: 1, content: 'Post1' }] }], + ) + + await database.set('post', 1, { + author: { + $set: r => ({ + value: 123, + }), + }, + }) + await expect(database.select('user', {}, { posts: true, profile: true }).execute()).to.eventually.have.shape( + [{ id: 1, value: 123, profile: { name: 'Apple' }, posts: [{ id: 1, content: 'Post1' }] }], + ) + + await database.set('post', 1, { + author: null, + }) + await expect(database.select('user', {}, { posts: true, profile: true }).execute()).to.eventually.have.shape( + [{ id: 1, value: 123, profile: { name: 'Apple' }, posts: [] }], + ) + }) + + it('create oneToMany', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) + + posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post1' })) + posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post2' })) + + await database.set('user', 2, { + posts: { + $create: [ + { content: 'post1' }, + { content: 'post2' }, + ], + }, + }) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + + posts.push(database.tables['post'].create({ id: 101, author: { id: 1 }, content: 'post101' })) + await database.set('user', 1, row => ({ + value: $.add(row.id, 98), + posts: { + $create: { id: 101, content: 'post101' }, + }, + })) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + }) + + it('set oneToMany', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) + + posts[0].score = 2 + posts[1].score = 3 + await database.set('user', 1, row => ({ + posts: { + $set: r => ({ + score: $.add(row.id, r.id), + }), + }, + })) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + + posts[1].score = 13 + await database.set('user', 1, row => ({ + posts: { + $set: { + where: { score: { $gt: 2 } }, + update: r => ({ score: $.add(r.score, 10) }), + }, + }, + })) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + }) + + it('delete oneToMany', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) + + posts.splice(0, 1) + await database.set('user', {}, row => ({ + posts: { + $remove: r => $.eq(r.id, row.id), + }, + })) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + }) + + it('override oneToMany', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + const posts = await setup(database, 'post', postTable) + + posts[0].score = 2 + posts[1].score = 3 + await database.set('user', 1, row => ({ + posts: posts.slice(0, 2), + })) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + + posts[0].score = 4 + posts[1].score = 5 + await database.set('user', 1, { + posts: posts.slice(0, 2), + }) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + }) + + ignoreNullObject && it('connect / disconnect oneToMany', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + await setup(database, 'post', postTable) + + await database.set('user', 1, { + posts: { + $disconnect: {}, + $connect: { id: 3 }, + }, + }) + await expect(database.get('user', 1, ['posts'])).to.eventually.have.shape([{ + posts: [ + { id: 3 }, + ], + }]) + }) + + it('create/set manyToMany', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + await setup(database, 'post', postTable) + await setup(database, 'tag', []) + + await setup(database, Relation.buildAssociationTable('post', 'tag') as any, []) + + await database.set('post', 2, { + tags: { + $create: { + id: 1, + name: 'Tag1', + }, + }, + }) + await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([ + { id: 1, name: 'Tag1' }, + ]) + + await database.set('post', 2, row => ({ + tags: { + $set: r => ({ + name: $.concat(r.name, '2'), + }), + }, + })) + await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([ + { id: 1, name: 'Tag12' }, + ]) + }) + + it('connect / disconnect manyToMany', async () => { + await setup(database, 'user', userTable) + await setup(database, 'profile', profileTable) + await setup(database, 'post', postTable) + await setup(database, 'tag', tagTable) + + await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + + await database.set('post', 2, { + tags: { + $disconnect: {}, + }, + }) + await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').deep.equal([]) + + await database.set('post', 2, row => ({ + tags: { + $connect: r => $.eq(r.id, row.id), + }, + })) + await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([{ + id: 2, + }]) + }) + + it('query relation', async () => { + await setup(database, 'user', userTable) + const posts = await setup(database, 'post', postTable) + await setup(database, 'tag', tagTable) + await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + + posts.filter(post => post2TagTable.some(p2t => p2t.post?.id === post.id && p2t.tag?.id === 1)).forEach(post => post.score! += 10) + await database.set('post', { + tags: { + $some: { + id: 1, + }, + }, + }, row => ({ + score: $.add(row.score, 10), + })) + await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + }) + + it('shared manyToMany', async () => { + await setup(database, 'login', [ + { id: '1', platform: 'sandbox', name: 'Bot1' }, + { id: '2', platform: 'sandbox', name: 'Bot2' }, + { id: '3', platform: 'sandbox', name: 'Bot3' }, + { id: '1', platform: 'whitebox', name: 'Bot1' }, + ]) + await setup(database, 'guild', [ + { id: '1', platform2: 'sandbox', name: 'Guild1' }, + { id: '2', platform2: 'sandbox', name: 'Guild2' }, + { id: '3', platform2: 'sandbox', name: 'Guild3' }, + { id: '1', platform2: 'whitebox', name: 'Guild1' }, + ]) + await setup(database, Relation.buildAssociationTable('login', 'guild') as any, []) + + await database.set('login', { + id: '1', + platform: 'sandbox', + }, { + guilds: { + $connect: { + id: { + $or: ['1', '2'], + }, + }, + }, + }) + await expect(database.get('login', { + id: '1', + platform: 'sandbox', + }, ['guilds'])).to.eventually.have.nested.property('[0].guilds').with.length(2) + + await database.set('login', { + id: '1', + platform: 'sandbox', + }, { + guilds: { + $disconnect: { + id: '2', + }, + }, + }) + + await expect(database.get('login', { + id: '1', + platform: 'sandbox', + }, ['guilds'])).to.eventually.have.nested.property('[0].guilds').with.length(1) + + + await database.create('guild', { + id: '4', + platform2: 'sandbox', + name: 'Guild4', + logins: { + $upsert: [ + { id: '1' }, + { id: '2' }, + ], + }, + }) + + // console.dir(await database.get('login', {}, { include: { guilds: true } }), { depth: 10 }) + // console.dir(await database.get('guild', {}, { include: { logins: true } }), { depth: 10 }) + + await expect(database.get('login', { platform: 'sandbox' }, ['id', 'guilds'])).to.eventually.have.shape([ + { id: '1', guilds: [{ id: '1' }, { id: '4' }] }, + { id: '2', guilds: [{ id: '4' }] }, + { id: '3', guilds: [] }, + ]) + + await expect(database.get('guild', { platform2: 'sandbox' }, ['id', 'logins'])).to.eventually.have.shape([ + { id: '1', logins: [{ id: '1' }] }, + { id: '2', logins: [] }, + { id: '3', logins: [] }, + { id: '4', logins: [{ id: '1' }, { id: '2' }] }, + ]) + }) + // it('explicit manyToMany', async () => { + // await setup(database, 'login', [ + // // { id: '1', platform: 'sandbox', name: 'Bot1' }, + // // { id: '2', platform: 'sandbox', name: 'Bot2' }, + // // { id: '3', platform: 'sandbox', name: 'Bot3' }, + // ]) + // await setup(database, 'guild', [ + // { id: '1', platform2: 'sandbox', name: 'Guild1' }, + // { id: '2', platform2: 'sandbox', name: 'Guild2' }, + // { id: '3', platform2: 'sandbox', name: 'Guild3' }, + // ]) + // await setup(database, Relation.buildAssociationTable('login', 'guild') as any, []) + // }) + } + + // export function misc(database: Database) { + // it('unsupported', async () => { + // await setup(database, 'user', userTable) + // await setup(database, 'post', postTable) + // await setup(database, 'tag', tagTable) + + // await expect(database.set('post', 1, { + // tags: [], + // })).to.eventually.be.rejected + + // await expect(database.set('post', 1, { + // tags: { + // $remove: {}, // }, - // }, - // })) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - // }) - - // it('delete oneToMany', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) - - // posts.splice(0, 1) - // await database.set('user', {}, row => ({ - // posts: { - // $remove: r => $.eq(r.id, row.id), - // }, - // })) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - // }) - - // it('override oneToMany', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'profile', profileTable) - // const posts = await setup(database, 'post', postTable) - - // posts[0].score = 2 - // posts[1].score = 3 - // await database.set('user', 1, row => ({ - // posts: posts.slice(0, 2), - // })) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - - // posts[0].score = 4 - // posts[1].score = 5 - // await database.upsert('user', [{ - // id: 1, - // posts: posts.slice(0, 2), - // }]) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - // }) - - // ignoreNullObject && it('connect / disconnect oneToMany', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'profile', profileTable) - // await setup(database, 'post', postTable) - - // await database.set('user', 1, { - // posts: { - // $disconnect: {}, - // $connect: { id: 3 }, - // }, - // }) - // await expect(database.get('user', 1, ['posts'])).to.eventually.have.shape([{ - // posts: [ - // { id: 3 }, - // ], - // }]) - // }) - - // it('connect / disconnect manyToMany', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'profile', profileTable) - // await setup(database, 'post', postTable) - // await setup(database, 'tag', tagTable) - - // await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) - - // await database.set('post', 2, { - // tags: { - // $disconnect: {}, - // }, - // }) - // await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').deep.equal([]) - - // await database.set('post', 2, row => ({ - // tags: { - // $connect: r => $.eq(r.id, row.id), - // }, - // })) - // await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([{ - // id: 2, - // }]) - // }) - - // it('query relation', async () => { - // await setup(database, 'user', userTable) - // const posts = await setup(database, 'post', postTable) - // await setup(database, 'tag', tagTable) - // await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) - - // posts.filter(post => post2TagTable.some(p2t => p2t.post?.id === post.id && p2t.tag?.id === 1)).forEach(post => post.score! += 10) - // await database.set('post', { - // tags: { - // $some: { - // id: 1, + // })).to.eventually.be.rejected + + // await expect(database.set('post', 1, { + // tags: { + // $set: {}, // }, - // }, - // }, row => ({ - // score: $.add(row.score, 10), - // })) - // await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - // }) - - // it('shared manyToMany', async () => { - // await setup(database, 'login', [ - // { id: '1', platform: 'sandbox', name: 'Bot1' }, - // { id: '2', platform: 'sandbox', name: 'Bot2' }, - // { id: '3', platform: 'sandbox', name: 'Bot3' }, - // ]) - // await setup(database, 'guild', [ - // { id: '1', platform2: 'sandbox', name: 'Guild1' }, - // { id: '2', platform2: 'sandbox', name: 'Guild2' }, - // { id: '3', platform2: 'sandbox', name: 'Guild3' }, - // ]) - // await setup(database, Relation.buildAssociationTable('login', 'guild') as any, []) - - // await database.set('login', { - // id: '1', - // platform: 'sandbox', - // }, { - // guilds: { - // $connect: { - // id: { - // $or: ['1', '2'] - // }, + // })).to.eventually.be.rejected + + // await expect(database.set('post', 1, { + // tags: { + // $create: {}, // }, - // }, + // })).to.eventually.be.rejected // }) - // await expect(database.get('login', { - // id: '1', - // platform: 'sandbox', - // }, ['guilds'])).to.eventually.have.nested.property('[0].guilds').with.length(2) - - // // await database.set('post', 2, row => ({ - // // tags: { - // // $connect: r => $.eq(r.id, row.id), - // // }, - // // })) - // // await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([{ - // // id: 2, - // // }]) - // }) - - // } - - // export function misc(database: Database) { - // it('unsupported', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'post', postTable) - // await setup(database, 'tag', tagTable) - - // await expect(database.set('post', 1, { - // tags: [], - // })).to.eventually.be.rejected - - // await expect(database.set('post', 1, { - // tags: { - // $remove: {}, - // }, - // })).to.eventually.be.rejected - - // await expect(database.set('post', 1, { - // tags: { - // $set: {}, - // }, - // })).to.eventually.be.rejected - - // await expect(database.set('post', 1, { - // tags: { - // $create: {}, - // }, - // })).to.eventually.be.rejected - // }) - // } + // } } export default RelationTests From 6949c6e7de2f0df9f3f608c786f76bba0d867ca7 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Sun, 2 Jun 2024 20:56:42 +0800 Subject: [PATCH 04/17] fix --- packages/tests/src/relation.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index f5b1694c..871b33dd 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -111,25 +111,25 @@ function RelationTests(database: Database) { // type: 'expr', // nullable: true, // }, - // author: { - // type: 'manyToOne', - // table: 'user', - // target: 'posts', - // }, + author: { + type: 'manyToOne', + table: 'user', + target: 'posts', + }, }, { autoInc: true, }) - database.relate({ - many: false, - table: 'user', - field: 'posts', - }, { - many: true, - table: 'post', - field: 'author', - nullable: true, - }) + // database.relate({ + // many: false, + // table: 'user', + // field: 'posts', + // }, { + // many: true, + // table: 'post', + // field: 'author', + // nullable: true, + // }) database.extend('tag', { id: 'unsigned', From 952be271eb22cc9f1ef58a6313e4a98b2ba03def Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Sun, 2 Jun 2024 23:14:05 +0800 Subject: [PATCH 05/17] fix: coverage --- packages/core/src/database.ts | 20 +-- packages/tests/src/relation.ts | 264 ++++++++++++++++++++------------- 2 files changed, 174 insertions(+), 110 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 0d314fc7..ab02608e 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -44,22 +44,24 @@ export namespace Join2 { export type Predicate> = (args: Parameters) => Eval.Expr } +type CreateMap = { [K in keyof T]?: Create } + export type Create = | T extends Values ? T - : T extends Values[] ? { [K in keyof T]?: Create } | + : T extends (infer I extends Values)[] ? CreateMap[] | { - $create?: MaybeArray<{ [K in keyof T]?: Create }> - $upsert?: MaybeArray<{ [K in keyof T]?: Create }> - $connect?: Query.Expr> + $create?: MaybeArray> + $upsert?: MaybeArray> + $connect?: Query.Expr> } - : T extends Values ? { [K in keyof T]?: Create } | + : T extends Values ? CreateMap | { - $create?: { [K in keyof T]?: Create } - $upsert?: { [K in keyof T]?: Create } + $create?: CreateMap + $upsert?: CreateMap $connect?: Query.Expr> } : T extends (infer U)[] ? DeepPartial[] - : T extends object ? { [K in keyof T]?: Create } + : T extends object ? CreateMap : T export class Database extends Service { @@ -548,7 +550,7 @@ export class Database extends Servi await this.upsert(relation.table as any, [value.$upsert]) relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) } else if (value.$connect) { - const result = relation.references.every(k => value.$connect![k] !== undefined) ? value.$connect + const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] : await this.get(relation.table as any, value.$connect as any) if (result.length !== 1) throw new Error('relation not found') relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index 871b33dd..4ec47a5f 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -39,7 +39,6 @@ interface Post2Tag { tag?: Tag } - interface GuildSyncRef { syncAt: number } @@ -107,10 +106,6 @@ function RelationTests(database: Database) { id: 'unsigned', score: 'unsigned', content: 'string', - // extraFields: { - // type: 'expr', - // nullable: true, - // }, author: { type: 'manyToOne', table: 'user', @@ -120,24 +115,9 @@ function RelationTests(database: Database) { autoInc: true, }) - // database.relate({ - // many: false, - // table: 'user', - // field: 'posts', - // }, { - // many: true, - // table: 'post', - // field: 'author', - // nullable: true, - // }) - database.extend('tag', { id: 'unsigned', name: 'string', - // extraFields: { - // type: 'expr', - // nullable: true, - // }, posts: { type: 'manyToMany', table: 'post', @@ -170,7 +150,7 @@ function RelationTests(database: Database) { platform: 'string', name: 'string', }, { - primary: ['id', 'platform'] + primary: ['id', 'platform'], }) database.extend('guild', { @@ -184,32 +164,14 @@ function RelationTests(database: Database) { shared: { platform2: 'platform' }, }, }, { - primary: ['id', 'platform2'] + primary: ['id', 'platform2'], }) - // database.relate({ - // many: true, - // table: 'guild', - // field: 'logins', - // }, { - // many: true, - // table: 'login', - // field: 'guilds', - // nullable: true, - // }, { - // shared: { - // platform2: 'platform' - // }, - // extra: { - // syncAt: 'unsigned', - // } - // }) - database.extend('guildSync', { syncAt: 'unsigned', platform: 'string', - "guild.id": 'string', - "login.id": 'string', + 'guild.id': 'string', + 'login.id': 'string', guild: { type: 'manyToOne', table: 'guild', @@ -221,9 +183,9 @@ function RelationTests(database: Database) { table: 'login', target: 'syncs', fields: ['login.id', 'platform'], - } + }, }, { - primary: ['platform', 'guild.id', 'login.id'] + primary: ['platform', 'guild.id', 'login.id'], }) async function setupAutoInc(database: Database, name: K, length: number) { @@ -651,7 +613,7 @@ namespace RelationTests { content: 'new post', author: { id: 2, - } + }, }) await expect(database.get('post', 1, ['author'])).to.eventually.have.shape([{ @@ -726,6 +688,87 @@ namespace RelationTests { ) }) + it('upsert / connect oneToMany / manyToOne', async () => { + await setup(database, 'user', []) + await setup(database, 'profile', []) + await setup(database, 'post', []) + + await database.create('user', { + id: 1, + value: 1, + posts: { + $upsert: [ + { + id: 1, + content: 'post1', + }, + { + id: 2, + content: 'post2', + }, + ], + }, + }) + + await expect(database.select('user', 1, { posts: true }).execute()).to.eventually.have.shape([ + { + id: 1, + value: 1, + posts: [ + { id: 1, content: 'post1' }, + { id: 2, content: 'post2' }, + ], + }, + ]) + + await database.create('user', { + id: 2, + value: 2, + posts: { + $create: [ + { + id: 3, + content: 'post3', + author: { + $upsert: { + id: 2, + value: 3, + }, + }, + }, + { + id: 4, + content: 'post4', + author: { + $connect: { + id: 1, + }, + }, + }, + ], + }, + }) + + await expect(database.select('user', {}, { posts: true }).execute()).to.eventually.have.shape([ + { + id: 1, + value: 1, + posts: [ + { id: 1, content: 'post1' }, + { id: 2, content: 'post2' }, + { id: 4, content: 'post4' }, + ], + }, + { + id: 2, + value: 3, + posts: [ + { id: 3, content: 'post3' }, + ], + }, + ]) + }) + it('manyToOne', async () => { const users = await setup(database, 'user', []) await setup(database, 'post', []) @@ -776,6 +819,7 @@ namespace RelationTests { await setup(database, 'user', []) await setup(database, 'post', []) await setup(database, 'tag', []) + await setup(database, Relation.buildAssociationTable('post', 'tag') as any, []) for (const user of userTable) { await database.create('user', { @@ -784,7 +828,7 @@ namespace RelationTests { ...post, tags: { $upsert: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), - } + }, })), }) } @@ -800,6 +844,73 @@ namespace RelationTests { ) }) + it('manyToMany expr', async () => { + await setup(database, 'user', []) + await setup(database, 'post', []) + await setup(database, 'tag', []) + await setup(database, Relation.buildAssociationTable('post', 'tag') as any, []) + + await database.create('post', { + id: 1, + content: 'post1', + author: { + $create: { + id: 1, + value: 1, + }, + }, + tags: { + $create: [ + { + name: 'tag1', + }, + { + name: 'tag2', + }, + ], + }, + }) + + await database.create('post', { + id: 2, + content: 'post2', + author: { + $connect: { + id: 1, + }, + }, + tags: { + $connect: { + name: 'tag1', + }, + }, + }) + + await expect(database.select('user', {}, { posts: { tags: true } }).execute()).to.eventually.have.shape([ + { + id: 1, + value: 1, + posts: [ + { + id: 1, + content: 'post1', + tags: [ + { name: 'tag1' }, + { name: 'tag2' }, + ], + }, + { + id: 2, + content: 'post2', + tags: [ + { name: 'tag1' }, + ], + }, + ], + }, + ]) + }) + it('explicit manyToMany', async () => { await setup(database, 'login', []) await setup(database, 'guild', [ @@ -819,7 +930,7 @@ namespace RelationTests { syncAt: 123, guild: { $connect: { id: '1' }, - } + }, }, ], }, @@ -831,7 +942,6 @@ namespace RelationTests { }, { include: { syncs: { guild: true } }, })).to.eventually.have.nested.property('[0].syncs').with.length(1) - }) } @@ -916,7 +1026,7 @@ namespace RelationTests { await database.set('user', 1, { profile: { $set: r => ({ - name: $.concat(r.name, '3') + name: $.concat(r.name, '3'), }), }, }) @@ -925,7 +1035,6 @@ namespace RelationTests { ) }) - ignoreNullObject && it('manyToOne expr', async () => { await setup(database, 'user', []) await setup(database, 'profile', []) @@ -946,8 +1055,8 @@ namespace RelationTests { name: 'Apple', }, }, - } - } + }, + }, }) await expect(database.select('user', {}, { posts: true, profile: true }).execute()).to.eventually.have.shape( [{ id: 1, value: 0, profile: { name: 'Apple' }, posts: [{ id: 1, content: 'Post1' }] }], @@ -983,8 +1092,8 @@ namespace RelationTests { await database.set('user', 2, { posts: { $create: [ - { content: 'post1' }, - { content: 'post2' }, + { id: 4, content: 'post1' }, + { id: 5, content: 'post2' }, ], }, }) @@ -1204,7 +1313,6 @@ namespace RelationTests { platform: 'sandbox', }, ['guilds'])).to.eventually.have.nested.property('[0].guilds').with.length(1) - await database.create('guild', { id: '4', platform2: 'sandbox', @@ -1217,9 +1325,6 @@ namespace RelationTests { }, }) - // console.dir(await database.get('login', {}, { include: { guilds: true } }), { depth: 10 }) - // console.dir(await database.get('guild', {}, { include: { logins: true } }), { depth: 10 }) - await expect(database.get('login', { platform: 'sandbox' }, ['id', 'guilds'])).to.eventually.have.shape([ { id: '1', guilds: [{ id: '1' }, { id: '4' }] }, { id: '2', guilds: [{ id: '4' }] }, @@ -1233,50 +1338,7 @@ namespace RelationTests { { id: '4', logins: [{ id: '1' }, { id: '2' }] }, ]) }) - // it('explicit manyToMany', async () => { - // await setup(database, 'login', [ - // // { id: '1', platform: 'sandbox', name: 'Bot1' }, - // // { id: '2', platform: 'sandbox', name: 'Bot2' }, - // // { id: '3', platform: 'sandbox', name: 'Bot3' }, - // ]) - // await setup(database, 'guild', [ - // { id: '1', platform2: 'sandbox', name: 'Guild1' }, - // { id: '2', platform2: 'sandbox', name: 'Guild2' }, - // { id: '3', platform2: 'sandbox', name: 'Guild3' }, - // ]) - // await setup(database, Relation.buildAssociationTable('login', 'guild') as any, []) - // }) } - - // export function misc(database: Database) { - // it('unsupported', async () => { - // await setup(database, 'user', userTable) - // await setup(database, 'post', postTable) - // await setup(database, 'tag', tagTable) - - // await expect(database.set('post', 1, { - // tags: [], - // })).to.eventually.be.rejected - - // await expect(database.set('post', 1, { - // tags: { - // $remove: {}, - // }, - // })).to.eventually.be.rejected - - // await expect(database.set('post', 1, { - // tags: { - // $set: {}, - // }, - // })).to.eventually.be.rejected - - // await expect(database.set('post', 1, { - // tags: { - // $create: {}, - // }, - // })).to.eventually.be.rejected - // }) - // } } export default RelationTests From 85512325cd8967efe5097337f148d5a94672fd12 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 02:26:59 +0800 Subject: [PATCH 06/17] cov: 2 --- packages/core/src/database.ts | 124 +++++++++++++++++++++------------ packages/core/src/model.ts | 2 +- packages/tests/src/relation.ts | 69 ++++++++++++++++-- 3 files changed, 143 insertions(+), 52 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index ab02608e..2e583a77 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -479,10 +479,36 @@ export class Database extends Servi return sel._action('remove').execute() } - async create>(table: K, data: Create, options?: Driver.CreateOptions): Promise - async create>(table: K, data: any, options?: any): Promise { + async create>(table: K, data: Create): Promise + async create>(table: K, data: any): Promise { const sel = this.select(table) + const tasks = [''] + for (const key in data) { + if (data[key] && this.tables[table].fields[key]?.relation) { + const relation = this.tables[table].fields[key].relation + if (relation.type === 'oneToOne' && relation.required) tasks.push(key) + else if (relation.type === 'oneToMany') tasks.push(key) + else if (relation.type === 'manyToOne' && isEvalExpr(data[key])) tasks.unshift(key) + else if (relation.type === 'manyToMany') tasks.push(key) + } + } + + if (tasks.length === 1) { + const { primary, autoInc } = sel.model + if (!autoInc) { + const keys = makeArray(primary) + if (keys.some(key => getCell(data, key) === undefined)) { + throw new Error('missing primary key') + } + } + return sel._action('create', sel.model.create(data)).execute() + } else { + return this.ensureTransaction(database => database._createOrUpdate(table, data)) + } + } + private async _createOrUpdate>(table: K, data: any): Promise { + const sel = this.select(table) data = { ...data } const tasks = [''] for (const key in data) { @@ -498,15 +524,17 @@ export class Database extends Servi for (const key of tasks) { if (!key) { // create the plain data, with or without upsert - const { primary, autoInc } = sel.model, keys = makeArray(primary) + const { primary, autoInc } = sel.model + const keys = makeArray(primary) + let upsert = true if (keys.some(key => getCell(data, key) === undefined)) { if (!autoInc) { throw new Error('missing primary key') } else { - (options ??= {}).upsert = false + upsert = false } } - if (options?.upsert) { + if (upsert) { await sel._action('upsert', [sel.model.format(omit(data, tasks))], keys).execute() } else { Object.assign(data, await sel._action('create', sel.model.create(omit(data, tasks))).execute()) @@ -516,10 +544,10 @@ export class Database extends Servi const value = data[key] const relation = this.tables[table].fields[key]!.relation! if (relation.type === 'oneToOne') { - await this.create(relation.table as any, { + await this._createOrUpdate(relation.table as any, { ...value.$create ?? value, ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), - } as any, { upsert: true }) + } as any) // if (value.$create) { // } else if (value.$upsert) { @@ -530,10 +558,10 @@ export class Database extends Servi } else if (relation.type === 'oneToMany') { if (value.$create || !isEvalExpr(value)) { for (const item of makeArray(value.$create ?? value)) { - await this.create(relation.table as any, { + await this._createOrUpdate(relation.table as any, { ...item, ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), - }, { upsert: true }) + }) } } if (value.$upsert) { @@ -544,7 +572,7 @@ export class Database extends Servi } } else if (relation.type === 'manyToOne') { if (value.$create) { - const result = await this.create(relation.table as any, value.$create, { upsert: true }) + const result = await this._createOrUpdate(relation.table as any, value.$create) relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) } else if (value.$upsert) { await this.upsert(relation.table as any, [value.$upsert]) @@ -566,10 +594,10 @@ export class Database extends Servi const result: any[] = [] if (value.$create) { for (const item of makeArray(value.$create)) { - result.push(await this.create(relation.table as any, { + result.push(await this._createOrUpdate(relation.table as any, { ...item, ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(item, v.reference) ?? getCell(data, v.field)])), - }, { upsert: true })) + })) } } if (value.$upsert) { @@ -763,10 +791,10 @@ export class Database extends Servi if (value === null) { await this.remove(relation.table as any, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) } else if (value.$create) { - const result = await this.create(relation.table, { + const result = await this._createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...value.$create, - }, { upsert: true }) + }) if (!relation.fields.some(x => x in makeArray(model.primary))) { relation.references.forEach((k, i) => row[relation.fields[i]] = getCell(result, k)) } @@ -784,12 +812,12 @@ export class Database extends Servi } } else if (relation.type === 'manyToOne') { if (value === null) { - // relation.fields.forEach((x, i) => row[x] = null) await this.set( table, pick(model.format(row), makeArray(model.primary)), Object.fromEntries(relation.fields.map((k, i) => [k, null])) as any, ) + // relation.fields.forEach((x, i) => row[x] = null) } if (!isEvalExpr(value)) { value = { $upsert: value } as any @@ -805,8 +833,7 @@ export class Database extends Servi ) } if (value.$create) { - // throw new Error('cannot create manyToOne relation in set') - const result = await this.create(relation.table as any, value.$create, { upsert: true }) + const result = await this._createOrUpdate(relation.table as any, value.$create) await this.set( table, omit(model.format(row), makeArray(model.primary)), @@ -828,7 +855,7 @@ export class Database extends Servi ) } if (value.$connect) { - const result = await this.get(relation.table as any, value.$connect)?.[0] + const result = (await this.get(relation.table as any, value.$connect))?.[0] if (!result) throw new Error('cannot find relation') await this.set( table, @@ -838,8 +865,8 @@ export class Database extends Servi } } else if (relation.type === 'oneToMany') { if (Array.isArray(value)) { - // When should we do upsert - value = { $remove: {}, $create: value } + // default to upsert, this will block nested relation update + value = { $remove: {}, $upsert: value } } if (value.$remove) { await this.remove(relation.table, (r: any) => Eval.query(r, { @@ -847,6 +874,15 @@ export class Database extends Servi ...(typeof value.$remove === 'function' ? { $expr: value.$remove(r) } : value.$remove), }) as any) } + if (value.$disconnect) { + await this.set(relation.table, + (r: any) => Eval.query(r, { + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...(typeof value.$disconnect === 'function' ? { $expr: value.$disconnect } : value.$disconnect), + } as any), + Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, + ) + } if (value.$set) { for (const setexpr of makeArray(value.$set) as any[]) { const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] @@ -861,10 +897,10 @@ export class Database extends Servi } if (value.$create) { for (const item of makeArray(value.$create)) { - await this.create(relation.table, { + await this._createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...item, - }, { upsert: true }) + }) } } if (value.$upsert) { @@ -873,15 +909,6 @@ export class Database extends Servi ...r, }))) } - if (value.$disconnect) { - await this.set(relation.table, - (r: any) => Eval.query(r, { - ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), - ...(typeof value.$disconnect === 'function' ? { $expr: value.$disconnect } : value.$disconnect), - } as any), - Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, - ) - } if (value.$connect) { await this.set(relation.table, value.$connect, @@ -897,14 +924,14 @@ export class Database extends Servi reference: y, }] as const) if (Array.isArray(value)) { - // When should we do upsert - value = { $disconnect: {}, $connect: value } + // default to upsert, this will block nested relation update + value = { $disconnect: {}, $upsert: value } } if (value.$remove) { const rows = await this.select(assocTable, { ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), ...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, - [relation.table]: value.$disconnect, + [relation.table]: value.$remove, }, null).execute() await this.remove(assocTable, r => Eval.in( [...shared.map(([k, v]) => r[k]), ...fields.map(x => r[x]), ...references.map(x => r[x])], @@ -915,6 +942,17 @@ export class Database extends Servi rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...references.map(x => getCell(r, x))]), )) } + if (value.$disconnect) { + const rows = await this.select(assocTable, { + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), + ...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + [relation.table]: value.$disconnect, + }, null).execute() + await this.remove(assocTable, r => Eval.in( + [...shared.map(([k, v]) => r[k]), ...fields.map(x => r[x]), ...references.map(x => r[x])], + rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), + )) + } if (value.$set) { for (const setexpr of makeArray(value.$set) as any[]) { const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] @@ -935,10 +973,10 @@ export class Database extends Servi if (value.$create) { const result: any[] = [] for (const item of makeArray(value.$create)) { - result.push(await this.create(relation.table, { + result.push(await this._createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...item, - }, { upsert: true })) + })) } await this.upsert(assocTable, result.map(r => ({ ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), @@ -951,17 +989,11 @@ export class Database extends Servi ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...r, }))) - } - if (value.$disconnect) { - const rows = await this.select(assocTable, { + await this.upsert(assocTable, makeArray(value.$upsert).map(r => ({ ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(row, v.field)])), - ...Object.fromEntries(fields.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, - [relation.table]: value.$disconnect, - }, null).execute() - await this.remove(assocTable, r => Eval.in( - [...shared.map(([k, v]) => r[k]), ...fields.map(x => r[x]), ...references.map(x => r[x])], - rows.map(r => [...shared.map(([k, v]) => getCell(r, k)), ...fields.map(x => getCell(r, x)), ...references.map(x => getCell(r, x))]), - )) + ...Object.fromEntries(fields.map((k, i) => [k, row[relation.fields[i]]])), + ...Object.fromEntries(references.map((k, i) => [k, r[relation.references[i] as any]])), + })) as any) } if (value.$connect) { const rows = await this.get(relation.table, (r: any) => Eval.query(r, { diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 47c4d511..444435a8 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -60,7 +60,7 @@ export namespace Relation { export interface Modifier { $create?: MaybeArray> - $upsert?: DeepPartial[] + $upsert?: MaybeArray> $set?: MaybeArray> $remove?: Query.Expr> | Selection.Callback $connect?: Query.Expr> | Selection.Callback diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index 4ec47a5f..b60d66ea 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -1086,8 +1086,10 @@ namespace RelationTests { await setup(database, 'profile', profileTable) const posts = await setup(database, 'post', postTable) - posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post1' })) - posts.push(database.tables['post'].create({ id: posts.length + 1, author: { id: 2 }, content: 'post2' })) + posts.push(database.tables['post'].create({ id: 4, author: { id: 2 }, content: 'post1' })) + posts.push(database.tables['post'].create({ id: 5, author: { id: 2 }, content: 'post2' })) + posts.push(database.tables['post'].create({ id: 6, author: { id: 2 }, content: 'post1' })) + posts.push(database.tables['post'].create({ id: 7, author: { id: 2 }, content: 'post2' })) await database.set('user', 2, { posts: { @@ -1095,6 +1097,10 @@ namespace RelationTests { { id: 4, content: 'post1' }, { id: 5, content: 'post2' }, ], + $upsert: [ + { id: 6, content: 'post1' }, + { id: 7, content: 'post2' }, + ], }, }) await expect(database.get('post', {})).to.eventually.have.deep.members(posts) @@ -1149,6 +1155,30 @@ namespace RelationTests { }, })) await expect(database.get('post', {})).to.eventually.have.deep.members(posts) + + await database.set('post', 2, { + author: { + $remove: {}, + $connect: { id: 2 }, + }, + }) + await database.set('post', 3, { + author: { + $disconnect: {}, + }, + }) + await expect(database.get('user', {}, { include: { posts: true } })).to.eventually.have.shape([ + { + id: 2, + posts: [ + { id: 2 }, + ], + }, + { + id: 3, + posts: [], + }, + ]) }) it('override oneToMany', async () => { @@ -1189,7 +1219,7 @@ namespace RelationTests { }]) }) - it('create/set manyToMany', async () => { + it('modify manyToMany', async () => { await setup(database, 'user', userTable) await setup(database, 'profile', profileTable) await setup(database, 'post', postTable) @@ -1203,21 +1233,50 @@ namespace RelationTests { id: 1, name: 'Tag1', }, + $upsert: [ + { + id: 2, + name: 'Tag2', + }, + { + id: 3, + name: 'Tag3', + }, + ], }, }) await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([ { id: 1, name: 'Tag1' }, + { id: 2, name: 'Tag2' }, + { id: 3, name: 'Tag3' }, ]) await database.set('post', 2, row => ({ tags: { $set: r => ({ - name: $.concat(r.name, '2'), + name: $.concat(r.name, row.content, '2'), }), + $remove: { + id: 3, + }, }, })) await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([ - { id: 1, name: 'Tag12' }, + { id: 1, name: 'Tag1B22' }, + { id: 2, name: 'Tag2B22' }, + ]) + + await database.set('post', 2, { + tags: [ + { id: 1, name: 'Tag1' }, + { id: 2, name: 'Tag2' }, + { id: 3, name: 'Tag3' }, + ], + }) + await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([ + { id: 1, name: 'Tag1' }, + { id: 2, name: 'Tag2' }, + { id: 3, name: 'Tag3' }, ]) }) From c6997d987d4dd060f9eeb833e9cb91b571de8f89 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 17:45:17 +0800 Subject: [PATCH 07/17] cov: fin --- packages/core/src/database.ts | 367 ++++++++++++++++------------ packages/memory/tests/index.spec.ts | 7 +- packages/tests/src/model.ts | 2 +- packages/tests/src/relation.ts | 190 ++++++++++++-- 4 files changed, 393 insertions(+), 173 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 2e583a77..a3c2be2e 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -463,7 +463,7 @@ export class Database extends Servi let baseUpdate = omit(update, relations.map(([key]) => key) as any) baseUpdate = sel.model.format(baseUpdate) for (const [key] of relations) { - await Promise.all(rows.map(row => this.processRelationUpdate(table, row, key, rawupdate(row as any)[key]))) + await Promise.all(rows.map(row => database.processRelationUpdate(table, row, key, rawupdate(row as any)[key]))) } return Object.keys(baseUpdate).length === 0 ? {} : await sel._action('set', baseUpdate).execute() }) @@ -486,7 +486,7 @@ export class Database extends Servi for (const key in data) { if (data[key] && this.tables[table].fields[key]?.relation) { const relation = this.tables[table].fields[key].relation - if (relation.type === 'oneToOne' && relation.required) tasks.push(key) + if (relation.type === 'oneToOne' && (relation.required || isEvalExpr(data[key]))) tasks.push(key) else if (relation.type === 'oneToMany') tasks.push(key) else if (relation.type === 'manyToOne' && isEvalExpr(data[key])) tasks.unshift(key) else if (relation.type === 'manyToMany') tasks.push(key) @@ -503,125 +503,8 @@ export class Database extends Servi } return sel._action('create', sel.model.create(data)).execute() } else { - return this.ensureTransaction(database => database._createOrUpdate(table, data)) - } - } - - private async _createOrUpdate>(table: K, data: any): Promise { - const sel = this.select(table) - data = { ...data } - const tasks = [''] - for (const key in data) { - if (data[key] && this.tables[table].fields[key]?.relation) { - const relation = this.tables[table].fields[key].relation - if (relation.type === 'oneToOne' && relation.required) tasks.push(key) - else if (relation.type === 'oneToMany') tasks.push(key) - else if (relation.type === 'manyToOne' && isEvalExpr(data[key])) tasks.unshift(key) - else if (relation.type === 'manyToMany') tasks.push(key) - } - } - - for (const key of tasks) { - if (!key) { - // create the plain data, with or without upsert - const { primary, autoInc } = sel.model - const keys = makeArray(primary) - let upsert = true - if (keys.some(key => getCell(data, key) === undefined)) { - if (!autoInc) { - throw new Error('missing primary key') - } else { - upsert = false - } - } - if (upsert) { - await sel._action('upsert', [sel.model.format(omit(data, tasks))], keys).execute() - } else { - Object.assign(data, await sel._action('create', sel.model.create(omit(data, tasks))).execute()) - } - continue - } - const value = data[key] - const relation = this.tables[table].fields[key]!.relation! - if (relation.type === 'oneToOne') { - await this._createOrUpdate(relation.table as any, { - ...value.$create ?? value, - ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), - } as any) - // if (value.$create) { - - // } else if (value.$upsert) { - - // } else if (value.$connect) { - - // } - } else if (relation.type === 'oneToMany') { - if (value.$create || !isEvalExpr(value)) { - for (const item of makeArray(value.$create ?? value)) { - await this._createOrUpdate(relation.table as any, { - ...item, - ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), - }) - } - } - if (value.$upsert) { - await this.upsert(relation.table as any, makeArray(value.$upsert).map(r => ({ - ...r, - ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), - }))) - } - } else if (relation.type === 'manyToOne') { - if (value.$create) { - const result = await this._createOrUpdate(relation.table as any, value.$create) - relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) - } else if (value.$upsert) { - await this.upsert(relation.table as any, [value.$upsert]) - relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) - } else if (value.$connect) { - const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] - : await this.get(relation.table as any, value.$connect as any) - if (result.length !== 1) throw new Error('relation not found') - relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) - } - } else if (relation.type === 'manyToMany') { - const assocTable = Relation.buildAssociationTable(relation.table as any, table) - const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) - const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) - const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { - field: x, - reference: y, - }] as const) - const result: any[] = [] - if (value.$create) { - for (const item of makeArray(value.$create)) { - result.push(await this._createOrUpdate(relation.table as any, { - ...item, - ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(item, v.reference) ?? getCell(data, v.field)])), - })) - } - } - if (value.$upsert) { - const upsert = makeArray(value.$upsert ?? value.$create).map(r => ({ - ...r, - ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(r, v.reference) ?? getCell(data, v.field)])), - })) - await this.upsert(relation.table as any, upsert) - result.push(...upsert) - } - if (value.$connect) { - for (const item of makeArray(value.$connect)) { - if (references.every(k => item[k] !== undefined)) result.push(item) - else result.push(...await this.get(relation.table as any, item)) - } - } - await this.upsert(assocTable as any, result.map(r => ({ - ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(r, v.reference) ?? getCell(data, v.field)])), - ...Object.fromEntries(fields.map((k, i) => [k, getCell(data, relation.fields[i])])), - ...Object.fromEntries(references.map((k, i) => [k, getCell(r, relation.references[i])])), - } as any))) - } + return this.ensureTransaction(database => database.createOrUpdate(table, data, false)) } - return data } async upsert>( @@ -784,62 +667,241 @@ export class Database extends Servi return { $expr: Eval.and(...results) } as any } + private async createOrUpdate>(table: K, data: any, upsert: boolean = true): Promise { + const sel = this.select(table) + data = { ...data } + const tasks = [''] + for (const key in data) { + if (data[key] && this.tables[table].fields[key]?.relation) { + const relation = this.tables[table].fields[key].relation + if (relation.type === 'oneToOne' && relation.required) tasks.push(key) + else if (relation.type === 'oneToOne' && isEvalExpr(data[key])) tasks.unshift(key) + else if (relation.type === 'oneToMany') tasks.push(key) + else if (relation.type === 'manyToOne' && isEvalExpr(data[key])) tasks.unshift(key) + else if (relation.type === 'manyToMany') tasks.push(key) + } + } + + for (const key of tasks) { + if (!key) { + // create the plain data, with or without upsert + const { primary, autoInc } = sel.model + const keys = makeArray(primary) + if (keys.some(key => getCell(data, key) === undefined)) { + if (!autoInc) { + throw new Error('missing primary key') + } else { + upsert = false + } + } + if (upsert) { + await sel._action('upsert', [sel.model.format(omit(data, tasks))], keys).execute() + } else { + Object.assign(data, await sel._action('create', sel.model.create(omit(data, tasks))).execute()) + } + continue + } + let value = data[key] + const relation: Relation.Config = this.tables[table].fields[key]!.relation! as any + if (relation.type === 'oneToOne') { + if (!isEvalExpr(value)) { + value = { $create: value } + } + if (value.$create || value.$upsert) { + const result = await this.createOrUpdate(relation.table, { + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), + ...value.$create ?? value.$upsert, + } as any) + if (!relation.required) { + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) + } + } else if (value.$connect) { + if (!relation.required) { + const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] + : await this.get(relation.table, value.$connect as any) + if (result.length !== 1) throw new Error('related row not found or not unique ') + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) + } else { + await this.set(relation.table, + value.$connect, + Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])) as any, + ) + } + } + } else if (relation.type === 'oneToMany') { + if (value.$create || !isEvalExpr(value)) { + for (const item of makeArray(value.$create ?? value)) { + await this.createOrUpdate(relation.table, { + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), + ...item, + }) + } + } + if (value.$upsert) { + await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), + ...r, + }))) + } + if (value.$connect) { + await this.set(relation.table, + value.$connect, + Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])) as any, + ) + } + } else if (relation.type === 'manyToOne') { + if (value.$create) { + const result = await this.createOrUpdate(relation.table, value.$create) + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) + } else if (value.$upsert) { + await this.upsert(relation.table, [value.$upsert]) + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) + } else if (value.$connect) { + const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] + : await this.get(relation.table, value.$connect as any) + if (result.length !== 1) throw new Error('related row not found or not unique ') + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) + } + } else if (relation.type === 'manyToMany') { + const assocTable = Relation.buildAssociationTable(relation.table, table) + const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) + const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) + const shared = Object.entries(relation.shared).map(([x, y]) => [Relation.buildSharedKey(x, y), { + field: x, + reference: y, + }] as const) + const result: any[] = [] + if (value.$create) { + for (const item of makeArray(value.$create)) { + result.push(await this.createOrUpdate(relation.table, { + ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(item, v.reference) ?? getCell(data, v.field)])), + ...item, + })) + } + } + if (value.$upsert) { + const upsert = makeArray(value.$upsert ?? value.$create).map(r => ({ + ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(r, v.reference) ?? getCell(data, v.field)])), + ...r, + })) + await this.upsert(relation.table, upsert) + result.push(...upsert) + } + if (value.$connect) { + for (const item of makeArray(value.$connect)) { + if (references.every(k => item[k] !== undefined)) result.push(item) + else result.push(...await this.get(relation.table, item)) + } + } + await this.upsert(assocTable as any, result.map(r => ({ + ...Object.fromEntries(shared.map(([k, v]) => [k, getCell(r, v.reference) ?? getCell(data, v.field)])), + ...Object.fromEntries(fields.map((k, i) => [k, getCell(data, relation.fields[i])])), + ...Object.fromEntries(references.map((k, i) => [k, getCell(r, relation.references[i])])), + } as any))) + } + } + return data + } + private async processRelationUpdate(table: any, row: any, key: any, value: Relation.Modifier) { const model = this.tables[table] const relation: Relation.Config = this.tables[table].fields[key]!.relation! as any if (relation.type === 'oneToOne') { if (value === null) { - await this.remove(relation.table as any, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) - } else if (value.$create) { - const result = await this._createOrUpdate(relation.table, { + value = relation.required ? { $remove: {} } : { $disconnect: {} } + } + if (typeof value === 'object' && !isEvalExpr(value)) { + value = { $upsert: value } + } + if (value.$remove) { + await this.remove(relation.table, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) + } + if (value.$disconnect) { + if (relation.required) { + await this.set(relation.table, + (r: any) => Eval.query(r, { + ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), + ...(typeof value.$disconnect === 'function' ? { $expr: value.$disconnect } : value.$disconnect), + } as any), + Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, + ) + } else { + await this.set( + table, + pick(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, null])) as any, + ) + } + } + if (value.$set || typeof value === 'function') { + await this.set( + relation.table, + Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + value.$set ?? value as any, + ) + } + if (value.$create) { + const result = await this.createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...value.$create, }) if (!relation.fields.some(x => x in makeArray(model.primary))) { relation.references.forEach((k, i) => row[relation.fields[i]] = getCell(result, k)) } - } else if (value.$set || typeof value === 'function') { - await this.set( - relation.table as any, - Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, - value.$set ?? value as any, - ) - } else { - await this.upsert(relation.table, makeArray(value.$upsert ?? value).map(r => ({ + } + if (value.$upsert) { + await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...r, }))) } + if (value.$connect) { + if (!relation.required) { + const result = await this.get(relation.table, value.$connect as any) + if (result.length !== 1) throw new Error('related row not found or not unique ') + await this.set( + table, + pick(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result[0], relation.references[i])])) as any, + ) + } else { + await this.set(relation.table, + value.$connect, + Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + ) + } + } } else if (relation.type === 'manyToOne') { if (value === null) { - await this.set( - table, - pick(model.format(row), makeArray(model.primary)), - Object.fromEntries(relation.fields.map((k, i) => [k, null])) as any, - ) - // relation.fields.forEach((x, i) => row[x] = null) + value = { $disconnect: {} } } - if (!isEvalExpr(value)) { + if (typeof value === 'object' && !isEvalExpr(value)) { value = { $upsert: value } as any } if (value.$remove) { await this.remove(relation.table, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any) } + if (value.$disconnect) { + await this.set( + table, + pick(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, null])) as any, + ) + } if (value.$set || typeof value === 'function') { await this.set( - relation.table as any, + relation.table, Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, value.$set ?? value as any, ) } if (value.$create) { - const result = await this._createOrUpdate(relation.table as any, value.$create) + const result = await this.createOrUpdate(relation.table, value.$create) await this.set( table, - omit(model.format(row), makeArray(model.primary)), + pick(model.format(row), makeArray(model.primary)), Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result, relation.references[i])])) as any, ) - // relation.references.forEach((k, i) => row[relation.fields[i]] = getCell(result, k)) } if (value.$upsert) { await this.upsert(relation.table, makeArray(value.$upsert).map(r => ({ @@ -847,20 +909,13 @@ export class Database extends Servi ...r, }))) } - if (value.$disconnect) { - await this.set( - table, - pick(model.format(row), makeArray(model.primary)), - Object.fromEntries(relation.fields.map((k, i) => [k, null])) as any, - ) - } if (value.$connect) { - const result = (await this.get(relation.table as any, value.$connect))?.[0] - if (!result) throw new Error('cannot find relation') + const result = (await this.get(relation.table, value.$connect)) + if (result.length !== 1) throw new Error('cannot find relation') await this.set( table, pick(model.format(row), makeArray(model.primary)), - Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result, relation.references[i])])) as any, + Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result[0], relation.references[i])])) as any, ) } } else if (relation.type === 'oneToMany') { @@ -883,8 +938,8 @@ export class Database extends Servi Object.fromEntries(relation.references.map((k, i) => [k, null])) as any, ) } - if (value.$set) { - for (const setexpr of makeArray(value.$set) as any[]) { + if (value.$set || typeof value === 'function') { + for (const setexpr of makeArray(value.$set ?? value) as any[]) { const [query, update] = setexpr.update ? [setexpr.where, setexpr.update] : [{}, setexpr] await this.set(relation.table, (r: any) => Eval.query(r, { @@ -897,7 +952,7 @@ export class Database extends Servi } if (value.$create) { for (const item of makeArray(value.$create)) { - await this._createOrUpdate(relation.table, { + await this.createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...item, }) @@ -973,7 +1028,7 @@ export class Database extends Servi if (value.$create) { const result: any[] = [] for (const item of makeArray(value.$create)) { - result.push(await this._createOrUpdate(relation.table, { + result.push(await this.createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...item, })) diff --git a/packages/memory/tests/index.spec.ts b/packages/memory/tests/index.spec.ts index 79a8ec50..0049ea6f 100644 --- a/packages/memory/tests/index.spec.ts +++ b/packages/memory/tests/index.spec.ts @@ -32,10 +32,13 @@ describe('@minatojs/driver-memory', () => { }, relation: { select: { - ignoreNullObject: false, + nullableComparator: false, + }, + create: { + nullableComparator: false, }, modify: { - ignoreNullObject: false, + nullableComparator: false, }, }, }) diff --git a/packages/tests/src/model.ts b/packages/tests/src/model.ts index 512b747e..4f0c5bf5 100644 --- a/packages/tests/src/model.ts +++ b/packages/tests/src/model.ts @@ -182,7 +182,7 @@ function ModelOperations(database: Database) { inner: { num: 'unsigned', text: 'string', - json: 'json', + json: 'object', embed: { type: 'object', inner: { diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index b60d66ea..f851624c 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -7,8 +7,8 @@ interface User { value?: number profile?: Profile posts?: Post[] - successor?: { id: number } - predecessor?: { id: number } + successor?: { id: number } & Record + predecessor?: { id: number } & Record } interface Profile { @@ -59,9 +59,6 @@ interface Guild extends GuildSyncRef { syncs?: GuildSync[] } -// interface GuildSync extends Login { } -// interface GuildSync extends Guild { } - interface GuildSync { syncAt?: number guild?: Guild @@ -234,11 +231,11 @@ namespace RelationTests { ] as any export interface RelationOptions { - ignoreNullObject?: boolean + nullableComparator?: boolean } export function select(database: Database, options: RelationOptions = {}) { - const { ignoreNullObject = true } = options + const { nullableComparator = true } = options it('basic support', async () => { const users = await setup(database, 'user', userTable) @@ -267,7 +264,7 @@ namespace RelationTests { ) }) - ignoreNullObject && it('self relation', async () => { + nullableComparator && it('self relation', async () => { const users = await setup(database, 'user', userTable) await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape( @@ -636,7 +633,9 @@ namespace RelationTests { }) } - export function create(database: Database) { + export function create(database: Database, options: RelationOptions = {}) { + const { nullableComparator = true } = options + it('basic support', async () => { await setup(database, 'user', []) await setup(database, 'profile', []) @@ -668,6 +667,78 @@ namespace RelationTests { ) }) + nullableComparator && it('nullable oneToOne', async () => { + await setup(database, 'user', []) + + await database.create('user', { + id: 1, + value: 1, + successor: { + $create: { + id: 2, + value: 2, + }, + }, + }) + await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + { id: 1, value: 1, successor: { id: 2, value: 2 } }, + { id: 2, value: 2 }, + ]) + + await database.create('user', { + id: 3, + value: 3, + predecessor: { + $upsert: { + id: 4, + value: 4, + }, + }, + }) + await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + { id: 1, value: 1, successor: { id: 2, value: 2 } }, + { id: 2, value: 2, successor: null }, + { id: 3, value: 3, successor: null }, + { id: 4, value: 4, successor: { id: 3, value: 3 } }, + ]) + + await database.remove('user', [2, 4]) + await database.create('user', { + id: 2, + value: 2, + successor: { + $connect: { + id: 1, + }, + }, + }) + await database.create('user', { + id: 4, + value: 4, + predecessor: { + $connect: { + id: 3, + }, + }, + }) + await database.create('user', { + id: 5, + value: 5, + successor: { + $connect: { + value: 3, + }, + }, + }) + await expect(database.select('user', {}, { successor: true }).orderBy('id').execute()).to.eventually.have.shape([ + { id: 1, value: 1, successor: { id: 2, value: 2 } }, + { id: 2, value: 2, successor: { id: 1, value: 1 } }, + { id: 3, value: 3, successor: { id: 4, value: 4 } }, + { id: 4, value: 4, successor: null }, + { id: 5, value: 5, successor: { id: 3, value: 3 } }, + ]) + }) + it('oneToMany', async () => { await setup(database, 'user', []) await setup(database, 'profile', []) @@ -725,6 +796,9 @@ namespace RelationTests { id: 2, value: 2, posts: { + $connect: { + id: 1, + }, $create: [ { id: 3, @@ -754,7 +828,6 @@ namespace RelationTests { id: 1, value: 1, posts: [ - { id: 1, content: 'post1' }, { id: 2, content: 'post2' }, { id: 4, content: 'post4' }, ], @@ -763,6 +836,7 @@ namespace RelationTests { id: 2, value: 3, posts: [ + { id: 1, content: 'post1' }, { id: 3, content: 'post3' }, ], }, @@ -946,7 +1020,7 @@ namespace RelationTests { } export function modify(database: Database, options: RelationOptions = {}) { - const { ignoreNullObject = true } = options + const { nullableComparator = true } = options it('oneToOne / manyToOne', async () => { const users = await setup(database, 'user', userTable) @@ -1035,7 +1109,68 @@ namespace RelationTests { ) }) - ignoreNullObject && it('manyToOne expr', async () => { + nullableComparator && it('nullable oneToOne', async () => { + await setup(database, 'user', []) + + await database.upsert('user', [ + { id: 1, value: 1, successor: { id: 2 } }, + { id: 2, value: 2, successor: { id: 1 } }, + { id: 3, value: 3, successor: { id: 4 } }, + { id: 4, value: 4, successor: { id: 3 } }, + ]) + await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + { id: 1, value: 1, successor: { id: 2, value: 2 } }, + { id: 2, value: 2, successor: { id: 1, value: 1 } }, + { id: 3, value: 3, successor: { id: 4, value: 4 } }, + { id: 4, value: 4, successor: { id: 3, value: 3 } }, + ]) + + await database.set('user', [1, 2], { + successor: null, + }) + await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + { id: 1, value: 1, successor: null }, + { id: 2, value: 2, successor: null }, + { id: 3, value: 3, successor: { id: 4, value: 4 } }, + { id: 4, value: 4, successor: { id: 3, value: 3 } }, + ]) + + await database.set('user', 3, { + predecessor: { + $disconnect: {}, + }, + successor: { + $disconnect: {}, + }, + }) + await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + { id: 1, value: 1, successor: null }, + { id: 2, value: 2, successor: null }, + { id: 3, value: 3, successor: null }, + { id: 4, value: 4, successor: null }, + ]) + + await database.set('user', 2, { + predecessor: { + $connect: { + id: 3, + }, + }, + successor: { + $connect: { + id: 1 + }, + }, + }) + await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + { id: 1, value: 1, successor: null }, + { id: 2, value: 2, successor: { id: 1, value: 1 } }, + { id: 3, value: 3, successor: { id: 2, value: 2 } }, + { id: 4, value: 4, successor: null }, + ]) + }) + + nullableComparator && it('manyToOne expr', async () => { await setup(database, 'user', []) await setup(database, 'profile', []) await setup(database, 'post', []) @@ -1143,7 +1278,7 @@ namespace RelationTests { await expect(database.get('post', {})).to.eventually.have.deep.members(posts) }) - it('delete oneToMany', async () => { + nullableComparator && it('delete oneToMany', async () => { await setup(database, 'user', userTable) await setup(database, 'profile', profileTable) const posts = await setup(database, 'post', postTable) @@ -1201,7 +1336,7 @@ namespace RelationTests { await expect(database.get('post', {})).to.eventually.have.deep.members(posts) }) - ignoreNullObject && it('connect / disconnect oneToMany', async () => { + nullableComparator && it('connect / disconnect oneToMany', async () => { await setup(database, 'user', userTable) await setup(database, 'profile', profileTable) await setup(database, 'post', postTable) @@ -1324,6 +1459,33 @@ namespace RelationTests { await expect(database.get('post', {})).to.eventually.have.deep.members(posts) }) + it('nested modify', async () => { + await setup(database, 'user', userTable) + await setup(database, 'post', postTable) + const profiles = await setup(database, 'profile', profileTable) + + profiles[0].name = 'Evil' + await database.set('user', 1, { + posts: { + $set: { + where: { id: { $gt: 1 } }, + update: { + author: { + $set: _ => ({ + profile: { + $set: _ => ({ + name: 'Evil', + }), + }, + }), + }, + }, + }, + }, + }) + await expect(database.get('profile', {})).to.eventually.have.deep.members(profiles) + }) + it('shared manyToMany', async () => { await setup(database, 'login', [ { id: '1', platform: 'sandbox', name: 'Bot1' }, From 9fd613366339c786cc4e05c5818dc115c227c183 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 18:29:45 +0800 Subject: [PATCH 08/17] feat: mismatch references/fields name --- packages/core/src/database.ts | 4 +- packages/tests/src/relation.ts | 149 +++++++++++++++++---------------- 2 files changed, 81 insertions(+), 72 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index a3c2be2e..1d0bd63b 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -160,13 +160,13 @@ export class Database extends Servi type: 'manyToOne', table: name, fields: [...shared, ...fields].map(x => x[0]), - references: [...Object.keys(relation.shared), ...relation.references], + references: [...Object.keys(relation.shared), ...relation.fields], }, [relation.table]: { type: 'manyToOne', table: relation.table, fields: [...shared, ...references].map(x => x[0]), - references: [...Object.values(relation.shared), ...relation.fields], + references: [...Object.values(relation.shared), ...relation.references], }, } as any, { primary: [...shared, ...fields, ...references].map(x => x[0]), diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index f851624c..4ea5ec40 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -7,8 +7,8 @@ interface User { value?: number profile?: Profile posts?: Post[] - successor?: { id: number } & Record - predecessor?: { id: number } & Record + successor?: Record & { id: number } + predecessor?: Record & { id: number } } interface Profile { @@ -18,7 +18,7 @@ interface Profile { } interface Post { - id: number + id2: number score?: number author?: User content?: string @@ -35,8 +35,8 @@ interface Tag { } interface Post2Tag { - post?: Post - tag?: Tag + post?: Post & { id: number } + tag?: Tag & { id: number } } interface GuildSyncRef { @@ -100,7 +100,7 @@ function RelationTests(database: Database) { }) database.extend('post', { - id: 'unsigned', + id2: 'unsigned', score: 'unsigned', content: 'string', author: { @@ -110,6 +110,7 @@ function RelationTests(database: Database) { }, }, { autoInc: true, + primary: 'id2', }) database.extend('tag', { @@ -139,7 +140,7 @@ function RelationTests(database: Database) { target: '_posts', }, }, { - primary: ['post', 'tag'], + primary: ['post.id', 'tag.id'], }) database.extend('login', { @@ -211,9 +212,9 @@ namespace RelationTests { ] const postTable: Post[] = [ - { id: 1, content: 'A1', author: { id: 1 } }, - { id: 2, content: 'B2', author: { id: 1 } }, - { id: 3, content: 'C3', author: { id: 2 } }, + { id2: 1, content: 'A1', author: { id: 1 } }, + { id2: 2, content: 'B2', author: { id: 1 } }, + { id2: 3, content: 'C3', author: { id: 2 } }, ] const tagTable: Tag[] = [ @@ -230,6 +231,14 @@ namespace RelationTests { { post: { id: 3 }, tag: { id: 3 } }, ] as any + const post2TagTable2: Post2Tag[] = [ + { post: { id2: 1 }, tag: { id: 1 } }, + { post: { id2: 1 }, tag: { id: 2 } }, + { post: { id2: 2 }, tag: { id: 1 } }, + { post: { id2: 2 }, tag: { id: 3 } }, + { post: { id2: 3 }, tag: { id: 3 } }, + ] as any + export interface RelationOptions { nullableComparator?: boolean } @@ -320,7 +329,7 @@ namespace RelationTests { const posts = await setup(database, 'post', postTable) const tags = await setup(database, 'tag', tagTable) const post2tags = await setup(database, 'post2tag', post2TagTable) - const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable2) // explicit manyToMany await expect(database.select('post', {}, { _tags: { tag: { _posts: { post: true } } } }).execute()).to.eventually.be.fulfilled @@ -328,12 +337,12 @@ namespace RelationTests { await expect(database.select('post', {}, { tags: { posts: true } }).execute()).to.eventually.have.shape( posts.map(post => ({ ...post, - tags: post2tags.filter(p2t => p2t.post?.id === post.id) + tags: post2tags.filter(p2t => p2t.post?.id === post.id2) .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) .filter(tag => tag) .map(tag => ({ ...tag, - posts: post2tags.filter(p2t => p2t.tag?.id === tag!.id).map(p2t => posts.find(post => post.id === p2t.post?.id)), + posts: post2tags.filter(p2t => p2t.tag?.id === tag!.id).map(p2t => posts.find(post => post.id2 === p2t.post?.id)), })), })), ) @@ -452,7 +461,7 @@ namespace RelationTests { const posts = await setup(database, 'post', postTable) const tags = await setup(database, 'tag', tagTable) const post2tags = await setup(database, 'post2tag', post2TagTable) - const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable2) await expect(database.get('post', { tags: { @@ -462,7 +471,7 @@ namespace RelationTests { }, })).to.eventually.have.shape(posts.slice(0, 2).map(post => ({ ...post, - tags: post2tags.filter(p2t => p2t.post?.id === post.id) + tags: post2tags.filter(p2t => p2t.post?.id === post.id2) .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) .filter(tag => tag), }))) @@ -475,7 +484,7 @@ namespace RelationTests { }, })).to.eventually.have.shape(posts.slice(2).map(post => ({ ...post, - tags: post2tags.filter(p2t => p2t.post?.id === post.id) + tags: post2tags.filter(p2t => p2t.post?.id === post.id2) .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) .filter(tag => tag), }))) @@ -488,7 +497,7 @@ namespace RelationTests { }, })).to.eventually.have.shape(posts.slice(2, 3).map(post => ({ ...post, - tags: post2tags.filter(p2t => p2t.post?.id === post.id) + tags: post2tags.filter(p2t => p2t.post?.id === post.id2) .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) .filter(tag => tag), }))) @@ -501,7 +510,7 @@ namespace RelationTests { }, })).to.eventually.have.shape(posts.slice(0, 1).map(post => ({ ...post, - tags: post2tags.filter(p2t => p2t.post?.id === post.id) + tags: post2tags.filter(p2t => p2t.post?.id === post.id2) .map(p2t => tags.find(tag => tag.id === p2t.tag?.id)) .filter(tag => tag), }))) @@ -513,7 +522,7 @@ namespace RelationTests { const posts = await setup(database, 'post', postTable) const tags = await setup(database, 'tag', tagTable) const post2tags = await setup(database, 'post2tag', post2TagTable) - const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + const re = await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable2) await expect(database.get('user', { posts: { @@ -541,7 +550,7 @@ namespace RelationTests { })).to.eventually.have.shape([tags[2]].map(tag => ({ ...tag, posts: post2tags.filter(p2t => p2t.tag?.id === tag.id) - .map(p2t => posts.find(post => post.id === p2t.post?.id)) + .map(p2t => posts.find(post => post.id2 === p2t.post?.id)) .filter(post => post), }))) }) @@ -581,12 +590,12 @@ namespace RelationTests { await database.set('user', 2, { posts: { $disconnect: { - id: 3, + id2: 3, }, }, }) await expect(database.select('post', { author: null }, null).execute()).to.eventually.have.shape([ - { id: 3 }, + { id2: 3 }, ]) await expect(database.select('user', { posts: { @@ -606,7 +615,7 @@ namespace RelationTests { await setup(database, 'post', []) await database.create('post', { - id: 1, + id2: 1, content: 'new post', author: { id: 2, @@ -770,11 +779,11 @@ namespace RelationTests { posts: { $upsert: [ { - id: 1, + id2: 1, content: 'post1', }, { - id: 2, + id2: 2, content: 'post2', }, ], @@ -786,8 +795,8 @@ namespace RelationTests { id: 1, value: 1, posts: [ - { id: 1, content: 'post1' }, - { id: 2, content: 'post2' }, + { id2: 1, content: 'post1' }, + { id2: 2, content: 'post2' }, ], }, ]) @@ -797,11 +806,11 @@ namespace RelationTests { value: 2, posts: { $connect: { - id: 1, + id2: 1, }, $create: [ { - id: 3, + id2: 3, content: 'post3', author: { $upsert: { @@ -811,7 +820,7 @@ namespace RelationTests { }, }, { - id: 4, + id2: 4, content: 'post4', author: { $connect: { @@ -828,16 +837,16 @@ namespace RelationTests { id: 1, value: 1, posts: [ - { id: 2, content: 'post2' }, - { id: 4, content: 'post4' }, + { id2: 2, content: 'post2' }, + { id2: 4, content: 'post4' }, ], }, { id: 2, value: 3, posts: [ - { id: 1, content: 'post1' }, - { id: 3, content: 'post3' }, + { id2: 1, content: 'post1' }, + { id2: 3, content: 'post3' }, ], }, ]) @@ -850,7 +859,7 @@ namespace RelationTests { users.push({ id: 1, value: 2 }) await database.create('post', { - id: 1, + id2: 1, content: 'post2', author: { $create: { @@ -863,7 +872,7 @@ namespace RelationTests { users[0].value = 3 await database.create('post', { - id: 2, + id2: 2, content: 'post3', author: { $create: { @@ -875,7 +884,7 @@ namespace RelationTests { await expect(database.get('user', {})).to.eventually.have.shape(users) await database.create('post', { - id: 3, + id2: 3, content: 'post4', author: { id: 1, @@ -883,9 +892,9 @@ namespace RelationTests { }) await expect(database.get('user', {})).to.eventually.have.shape(users) await expect(database.get('post', {}, { include: { author: true } })).to.eventually.have.shape([ - { id: 1, content: 'post2', author: { id: 1, value: 3 } }, - { id: 2, content: 'post3', author: { id: 1, value: 3 } }, - { id: 3, content: 'post4', author: { id: 1, value: 3 } }, + { id2: 1, content: 'post2', author: { id: 1, value: 3 } }, + { id2: 2, content: 'post3', author: { id: 1, value: 3 } }, + { id2: 3, content: 'post4', author: { id: 1, value: 3 } }, ]) }) @@ -901,7 +910,7 @@ namespace RelationTests { posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ ...post, tags: { - $upsert: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), + $upsert: post2TagTable.filter(p2t => p2t.post?.id === post.id2).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)).filter(x => !!x), }, })), }) @@ -912,7 +921,7 @@ namespace RelationTests { ...user, posts: postTable.filter(post => post.author?.id === user.id).map(post => ({ ...post, - tags: post2TagTable.filter(p2t => p2t.post?.id === post.id).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)), + tags: post2TagTable.filter(p2t => p2t.post?.id === post.id2).map(p2t => tagTable.find(tag => tag.id === p2t.tag?.id)), })), })), ) @@ -925,7 +934,7 @@ namespace RelationTests { await setup(database, Relation.buildAssociationTable('post', 'tag') as any, []) await database.create('post', { - id: 1, + id2: 1, content: 'post1', author: { $create: { @@ -946,7 +955,7 @@ namespace RelationTests { }) await database.create('post', { - id: 2, + id2: 2, content: 'post2', author: { $connect: { @@ -966,7 +975,7 @@ namespace RelationTests { value: 1, posts: [ { - id: 1, + id2: 1, content: 'post1', tags: [ { name: 'tag1' }, @@ -974,7 +983,7 @@ namespace RelationTests { ], }, { - id: 2, + id2: 2, content: 'post2', tags: [ { name: 'tag1' }, @@ -1165,7 +1174,7 @@ namespace RelationTests { await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ { id: 1, value: 1, successor: null }, { id: 2, value: 2, successor: { id: 1, value: 1 } }, - { id: 3, value: 3, successor: { id: 2, value: 2 } }, + { id: 3, value: 3, successor: { id: 2, value: 2 } }, { id: 4, value: 4, successor: null }, ]) }) @@ -1176,7 +1185,7 @@ namespace RelationTests { await setup(database, 'post', []) await database.create('post', { - id: 1, + id2: 1, content: 'Post1', }) @@ -1194,7 +1203,7 @@ namespace RelationTests { }, }) await expect(database.select('user', {}, { posts: true, profile: true }).execute()).to.eventually.have.shape( - [{ id: 1, value: 0, profile: { name: 'Apple' }, posts: [{ id: 1, content: 'Post1' }] }], + [{ id: 1, value: 0, profile: { name: 'Apple' }, posts: [{ id2: 1, content: 'Post1' }] }], ) await database.set('post', 1, { @@ -1205,7 +1214,7 @@ namespace RelationTests { }, }) await expect(database.select('user', {}, { posts: true, profile: true }).execute()).to.eventually.have.shape( - [{ id: 1, value: 123, profile: { name: 'Apple' }, posts: [{ id: 1, content: 'Post1' }] }], + [{ id: 1, value: 123, profile: { name: 'Apple' }, posts: [{ id2: 1, content: 'Post1' }] }], ) await database.set('post', 1, { @@ -1221,30 +1230,30 @@ namespace RelationTests { await setup(database, 'profile', profileTable) const posts = await setup(database, 'post', postTable) - posts.push(database.tables['post'].create({ id: 4, author: { id: 2 }, content: 'post1' })) - posts.push(database.tables['post'].create({ id: 5, author: { id: 2 }, content: 'post2' })) - posts.push(database.tables['post'].create({ id: 6, author: { id: 2 }, content: 'post1' })) - posts.push(database.tables['post'].create({ id: 7, author: { id: 2 }, content: 'post2' })) + posts.push(database.tables['post'].create({ id2: 4, author: { id: 2 }, content: 'post1' })) + posts.push(database.tables['post'].create({ id2: 5, author: { id: 2 }, content: 'post2' })) + posts.push(database.tables['post'].create({ id2: 6, author: { id: 2 }, content: 'post1' })) + posts.push(database.tables['post'].create({ id2: 7, author: { id: 2 }, content: 'post2' })) await database.set('user', 2, { posts: { $create: [ - { id: 4, content: 'post1' }, - { id: 5, content: 'post2' }, + { id2: 4, content: 'post1' }, + { id2: 5, content: 'post2' }, ], $upsert: [ - { id: 6, content: 'post1' }, - { id: 7, content: 'post2' }, + { id2: 6, content: 'post1' }, + { id2: 7, content: 'post2' }, ], }, }) await expect(database.get('post', {})).to.eventually.have.deep.members(posts) - posts.push(database.tables['post'].create({ id: 101, author: { id: 1 }, content: 'post101' })) + posts.push(database.tables['post'].create({ id2: 101, author: { id: 1 }, content: 'post101' })) await database.set('user', 1, row => ({ value: $.add(row.id, 98), posts: { - $create: { id: 101, content: 'post101' }, + $create: { id2: 101, content: 'post101' }, }, })) await expect(database.get('post', {})).to.eventually.have.deep.members(posts) @@ -1260,7 +1269,7 @@ namespace RelationTests { await database.set('user', 1, row => ({ posts: { $set: r => ({ - score: $.add(row.id, r.id), + score: $.add(row.id, r.id2), }), }, })) @@ -1286,7 +1295,7 @@ namespace RelationTests { posts.splice(0, 1) await database.set('user', {}, row => ({ posts: { - $remove: r => $.eq(r.id, row.id), + $remove: r => $.eq(r.id2, row.id), }, })) await expect(database.get('post', {})).to.eventually.have.deep.members(posts) @@ -1306,7 +1315,7 @@ namespace RelationTests { { id: 2, posts: [ - { id: 2 }, + { id2: 2 }, ], }, { @@ -1344,12 +1353,12 @@ namespace RelationTests { await database.set('user', 1, { posts: { $disconnect: {}, - $connect: { id: 3 }, + $connect: { id2: 3 }, }, }) await expect(database.get('user', 1, ['posts'])).to.eventually.have.shape([{ posts: [ - { id: 3 }, + { id2: 3 }, ], }]) }) @@ -1421,7 +1430,7 @@ namespace RelationTests { await setup(database, 'post', postTable) await setup(database, 'tag', tagTable) - await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable2) await database.set('post', 2, { tags: { @@ -1432,7 +1441,7 @@ namespace RelationTests { await database.set('post', 2, row => ({ tags: { - $connect: r => $.eq(r.id, row.id), + $connect: r => $.eq(r.id, row.id2), }, })) await expect(database.get('post', 2, ['tags'])).to.eventually.have.nested.property('[0].tags').with.shape([{ @@ -1444,9 +1453,9 @@ namespace RelationTests { await setup(database, 'user', userTable) const posts = await setup(database, 'post', postTable) await setup(database, 'tag', tagTable) - await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable) + await setup(database, Relation.buildAssociationTable('post', 'tag') as any, post2TagTable2) - posts.filter(post => post2TagTable.some(p2t => p2t.post?.id === post.id && p2t.tag?.id === 1)).forEach(post => post.score! += 10) + posts.filter(post => post2TagTable.some(p2t => p2t.post?.id === post.id2 && p2t.tag?.id === 1)).forEach(post => post.score! += 10) await database.set('post', { tags: { $some: { @@ -1468,7 +1477,7 @@ namespace RelationTests { await database.set('user', 1, { posts: { $set: { - where: { id: { $gt: 1 } }, + where: { id2: { $gt: 1 } }, update: { author: { $set: _ => ({ From 27aad3da66e5c3e757fc8b9fac9e31eb262d9567 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 18:33:39 +0800 Subject: [PATCH 09/17] fix: untyped callback --- packages/tests/src/relation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index 4ea5ec40..f54e2328 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -421,7 +421,7 @@ namespace RelationTests { await expect(database.get('user', { posts: { - $some: row => $.eq(row.id, 1), + $some: row => $.eq(row.id2, 1), }, })).to.eventually.have.shape(users.slice(0, 1).map(user => ({ ...user, From 965336cd9870a96a89574b51be1f0e6e4ab86d66 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 20:09:19 +0800 Subject: [PATCH 10/17] fix: trasaction consistency --- packages/core/src/database.ts | 46 +++++++++++++++++++--------------- packages/tests/src/relation.ts | 33 +++++++++++++++++++----- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 1d0bd63b..1e41072f 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -447,7 +447,7 @@ export class Database extends Servi update: Row.Computed>, ): Promise { const rawupdate = typeof update === 'function' ? update : () => update - const sel = this.select(table, query, null) + let sel = this.select(table, query, null) if (typeof update === 'function') update = update(sel.row) const primary = makeArray(sel.model.primary) if (primary.some(key => key in update)) { @@ -460,7 +460,8 @@ export class Database extends Servi if (relations.length) { return await this.ensureTransaction(async (database) => { const rows = await database.get(table, query) - let baseUpdate = omit(update, relations.map(([key]) => key) as any) + sel = database.select(table, query, null) + let baseUpdate = omit(rawupdate(sel.row), relations.map(([key]) => key) as any) baseUpdate = sel.model.format(baseUpdate) for (const [key] of relations) { await Promise.all(rows.map(row => database.processRelationUpdate(table, row, key, rawupdate(row as any)[key]))) @@ -482,18 +483,18 @@ export class Database extends Servi async create>(table: K, data: Create): Promise async create>(table: K, data: any): Promise { const sel = this.select(table) - const tasks = [''] + let hasRelation = false for (const key in data) { - if (data[key] && this.tables[table].fields[key]?.relation) { + if (data[key] !== undefined && this.tables[table].fields[key]?.relation) { const relation = this.tables[table].fields[key].relation - if (relation.type === 'oneToOne' && (relation.required || isEvalExpr(data[key]))) tasks.push(key) - else if (relation.type === 'oneToMany') tasks.push(key) - else if (relation.type === 'manyToOne' && isEvalExpr(data[key])) tasks.unshift(key) - else if (relation.type === 'manyToMany') tasks.push(key) + if (relation.type === 'oneToOne' && !relation.required && !isEvalExpr(data[key])) continue + if (relation.type === 'manyToOne' && !isEvalExpr(data[key])) continue + hasRelation = true + break } } - if (tasks.length === 1) { + if (!hasRelation) { const { primary, autoInc } = sel.model if (!autoInc) { const keys = makeArray(primary) @@ -672,7 +673,7 @@ export class Database extends Servi data = { ...data } const tasks = [''] for (const key in data) { - if (data[key] && this.tables[table].fields[key]?.relation) { + if (data[key] !== undefined && this.tables[table].fields[key]?.relation) { const relation = this.tables[table].fields[key].relation if (relation.type === 'oneToOne' && relation.required) tasks.push(key) else if (relation.type === 'oneToOne' && isEvalExpr(data[key])) tasks.unshift(key) @@ -845,8 +846,12 @@ export class Database extends Servi ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])), ...value.$create, }) - if (!relation.fields.some(x => x in makeArray(model.primary))) { - relation.references.forEach((k, i) => row[relation.fields[i]] = getCell(result, k)) + if (!relation.required) { + await this.set( + table, + pick(model.format(row), makeArray(model.primary)), + Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result, relation.references[i])])) as any, + ) } } if (value.$upsert) { @@ -856,18 +861,19 @@ export class Database extends Servi }))) } if (value.$connect) { - if (!relation.required) { - const result = await this.get(relation.table, value.$connect as any) + if (relation.required) { + await this.set(relation.table, + value.$connect, + Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + ) + } else { + const result = relation.references.every(k => value.$connect![k as any] !== undefined) ? [value.$connect] + : await this.get(relation.table, value.$connect as any) if (result.length !== 1) throw new Error('related row not found or not unique ') await this.set( table, pick(model.format(row), makeArray(model.primary)), - Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result[0], relation.references[i])])) as any, - ) - } else { - await this.set(relation.table, - value.$connect, - Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, + Object.fromEntries(relation.fields.map((k, i) => [k, getCell(result[0], relation.references[i])])) as any, ) } } diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index f54e2328..d3211fd4 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -279,7 +279,7 @@ namespace RelationTests { await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape( users.map(user => ({ ...user, - successor: users.find(successor => successor.id === user.successor?.id), + successor: users.find(successor => successor.id === user.successor?.id) ?? null, })), ) }) @@ -688,10 +688,17 @@ namespace RelationTests { value: 2, }, }, + predecessor: { + $create: { + id: 6, + value: 6, + }, + }, }) - await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + await expect(database.select('user', {}, { successor: true }).orderBy('id').execute()).to.eventually.have.shape([ { id: 1, value: 1, successor: { id: 2, value: 2 } }, - { id: 2, value: 2 }, + { id: 2, value: 2, successor: null }, + { id: 6, value: 6, successor: { id: 1, value: 1 } }, ]) await database.create('user', { @@ -704,11 +711,12 @@ namespace RelationTests { }, }, }) - await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ + await expect(database.select('user', {}, { successor: true }).orderBy('id').execute()).to.eventually.have.shape([ { id: 1, value: 1, successor: { id: 2, value: 2 } }, { id: 2, value: 2, successor: null }, { id: 3, value: 3, successor: null }, { id: 4, value: 4, successor: { id: 3, value: 3 } }, + { id: 6, value: 6 }, ]) await database.remove('user', [2, 4]) @@ -745,6 +753,7 @@ namespace RelationTests { { id: 3, value: 3, successor: { id: 4, value: 4 } }, { id: 4, value: 4, successor: null }, { id: 5, value: 5, successor: { id: 3, value: 3 } }, + { id: 6, value: 6, successor: { id: 1, value: 1 } }, ]) }) @@ -1124,9 +1133,21 @@ namespace RelationTests { await database.upsert('user', [ { id: 1, value: 1, successor: { id: 2 } }, { id: 2, value: 2, successor: { id: 1 } }, - { id: 3, value: 3, successor: { id: 4 } }, - { id: 4, value: 4, successor: { id: 3 } }, + { id: 3, value: 3 }, ]) + await database.set('user', 3, { + successor: { + $create: { + id: 4, + value: 4, + }, + }, + predecessor: { + $connect: { + id: 4, + }, + }, + }) await expect(database.select('user', {}, { successor: true }).execute()).to.eventually.have.shape([ { id: 1, value: 1, successor: { id: 2, value: 2 } }, { id: 2, value: 2, successor: { id: 1, value: 1 } }, From 64f93ba34dbd6fd8ceb464ff1c801a5859bc2daf Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 21:12:46 +0800 Subject: [PATCH 11/17] cov --- packages/core/src/database.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 1e41072f..f31dd0ad 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -720,7 +720,7 @@ export class Database extends Servi if (!relation.required) { const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] : await this.get(relation.table, value.$connect as any) - if (result.length !== 1) throw new Error('related row not found or not unique ') + if (result.length !== 1) throw new Error('related row not found or not unique') relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) } else { await this.set(relation.table, @@ -760,7 +760,7 @@ export class Database extends Servi } else if (value.$connect) { const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] : await this.get(relation.table, value.$connect as any) - if (result.length !== 1) throw new Error('related row not found or not unique ') + if (result.length !== 1) throw new Error('related row not found or not unique') relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) } } else if (relation.type === 'manyToMany') { @@ -867,9 +867,8 @@ export class Database extends Servi Object.fromEntries(relation.references.map((k, i) => [k, getCell(row, relation.fields[i])])) as any, ) } else { - const result = relation.references.every(k => value.$connect![k as any] !== undefined) ? [value.$connect] - : await this.get(relation.table, value.$connect as any) - if (result.length !== 1) throw new Error('related row not found or not unique ') + const result = await this.get(relation.table, value.$connect as any) + if (result.length !== 1) throw new Error('related row not found or not unique') await this.set( table, pick(model.format(row), makeArray(model.primary)), @@ -916,8 +915,8 @@ export class Database extends Servi }))) } if (value.$connect) { - const result = (await this.get(relation.table, value.$connect)) - if (result.length !== 1) throw new Error('cannot find relation') + const result = await this.get(relation.table, value.$connect) + if (result.length !== 1) throw new Error('related row not found or not unique') await this.set( table, pick(model.format(row), makeArray(model.primary)), From 2548ff0fdf4091ba9b0eccf79c9a0fa0739649ba Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 22:06:28 +0800 Subject: [PATCH 12/17] chore: clean, default behaviors --- packages/core/src/database.ts | 71 +++++++++++++++++------------------ packages/core/src/eval.ts | 2 + 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index f31dd0ad..5603f2f5 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -4,7 +4,7 @@ import { AtomicTypes, DeepPartial, FlatKeys, FlatPick, Flatten, getCell, Indexab import { Selection } from './selection.ts' import { Field, Model, Relation } from './model.ts' import { Driver } from './driver.ts' -import { Eval, isEvalExpr, Update } from './eval.ts' +import { Eval, isUpdateExpr, Update } from './eval.ts' import { Query } from './query.ts' import { Type } from './type.ts' @@ -487,8 +487,8 @@ export class Database extends Servi for (const key in data) { if (data[key] !== undefined && this.tables[table].fields[key]?.relation) { const relation = this.tables[table].fields[key].relation - if (relation.type === 'oneToOne' && !relation.required && !isEvalExpr(data[key])) continue - if (relation.type === 'manyToOne' && !isEvalExpr(data[key])) continue + if (relation.type === 'oneToOne' && !relation.required && !isUpdateExpr(data[key])) continue + if (relation.type === 'manyToOne' && !isUpdateExpr(data[key])) continue hasRelation = true break } @@ -676,9 +676,9 @@ export class Database extends Servi if (data[key] !== undefined && this.tables[table].fields[key]?.relation) { const relation = this.tables[table].fields[key].relation if (relation.type === 'oneToOne' && relation.required) tasks.push(key) - else if (relation.type === 'oneToOne' && isEvalExpr(data[key])) tasks.unshift(key) + else if (relation.type === 'oneToOne' && isUpdateExpr(data[key])) tasks.unshift(key) else if (relation.type === 'oneToMany') tasks.push(key) - else if (relation.type === 'manyToOne' && isEvalExpr(data[key])) tasks.unshift(key) + else if (relation.type === 'manyToOne' && isUpdateExpr(data[key])) tasks.unshift(key) else if (relation.type === 'manyToMany') tasks.push(key) } } @@ -702,35 +702,45 @@ export class Database extends Servi } continue } - let value = data[key] + const value: Relation.Modifier = data[key] const relation: Relation.Config = this.tables[table].fields[key]!.relation! as any if (relation.type === 'oneToOne') { - if (!isEvalExpr(value)) { - value = { $create: value } - } - if (value.$create || value.$upsert) { + if (value.$create || value.$upsert || !isUpdateExpr(value)) { const result = await this.createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), - ...value.$create ?? value.$upsert, + ...value.$create ?? value.$upsert ?? value, } as any) if (!relation.required) { relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) } } else if (value.$connect) { - if (!relation.required) { - const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] - : await this.get(relation.table, value.$connect as any) - if (result.length !== 1) throw new Error('related row not found or not unique') - relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) - } else { + if (relation.required) { await this.set(relation.table, value.$connect, Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])) as any, ) + } else { + const result = relation.references.every(k => value.$connect![k as any] !== undefined) ? [value.$connect] + : await this.get(relation.table, value.$connect as any) + if (result.length !== 1) throw new Error('related row not found or not unique') + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) } } + } else if (relation.type === 'manyToOne') { + if (value.$create || !isUpdateExpr(value)) { + const result = await this.createOrUpdate(relation.table, value.$create ?? value) + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) + } else if (value.$upsert) { + await this.upsert(relation.table, [value.$upsert]) + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) + } else if (value.$connect) { + const result = relation.references.every(k => value.$connect![k as any] !== undefined) ? [value.$connect] + : await this.get(relation.table, value.$connect as any) + if (result.length !== 1) throw new Error('related row not found or not unique') + relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) + } } else if (relation.type === 'oneToMany') { - if (value.$create || !isEvalExpr(value)) { + if (value.$create || Array.isArray(value)) { for (const item of makeArray(value.$create ?? value)) { await this.createOrUpdate(relation.table, { ...Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])), @@ -750,19 +760,6 @@ export class Database extends Servi Object.fromEntries(relation.references.map((k, i) => [k, getCell(data, relation.fields[i])])) as any, ) } - } else if (relation.type === 'manyToOne') { - if (value.$create) { - const result = await this.createOrUpdate(relation.table, value.$create) - relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result, k)) - } else if (value.$upsert) { - await this.upsert(relation.table, [value.$upsert]) - relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(value.$upsert, k)) - } else if (value.$connect) { - const result = relation.references.every(k => value.$connect![k] !== undefined) ? [value.$connect] - : await this.get(relation.table, value.$connect as any) - if (result.length !== 1) throw new Error('related row not found or not unique') - relation.references.forEach((k, i) => data[relation.fields[i]] = getCell(result[0], k)) - } } else if (relation.type === 'manyToMany') { const assocTable = Relation.buildAssociationTable(relation.table, table) const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) @@ -772,8 +769,8 @@ export class Database extends Servi reference: y, }] as const) const result: any[] = [] - if (value.$create) { - for (const item of makeArray(value.$create)) { + if (value.$create || Array.isArray(value)) { + for (const item of makeArray(value.$create ?? value)) { result.push(await this.createOrUpdate(relation.table, { ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(item, v.reference) ?? getCell(data, v.field)])), ...item, @@ -781,7 +778,7 @@ export class Database extends Servi } } if (value.$upsert) { - const upsert = makeArray(value.$upsert ?? value.$create).map(r => ({ + const upsert = makeArray(value.$upsert).map(r => ({ ...Object.fromEntries(shared.map(([, v]) => [v.reference, getCell(r, v.reference) ?? getCell(data, v.field)])), ...r, })) @@ -811,7 +808,7 @@ export class Database extends Servi if (value === null) { value = relation.required ? { $remove: {} } : { $disconnect: {} } } - if (typeof value === 'object' && !isEvalExpr(value)) { + if (typeof value === 'object' && !isUpdateExpr(value)) { value = { $upsert: value } } if (value.$remove) { @@ -880,7 +877,7 @@ export class Database extends Servi if (value === null) { value = { $disconnect: {} } } - if (typeof value === 'object' && !isEvalExpr(value)) { + if (typeof value === 'object' && !isUpdateExpr(value)) { value = { $upsert: value } as any } if (value.$remove) { @@ -975,7 +972,7 @@ export class Database extends Servi Object.fromEntries(relation.references.map((k, i) => [k, row[relation.fields[i]]])) as any, ) } - } else { + } else if (relation.type === 'manyToMany') { const assocTable = Relation.buildAssociationTable(table, relation.table) as Keys const fields = relation.fields.map(x => Relation.buildAssociationKey(x, table)) const references = relation.references.map(x => Relation.buildAssociationKey(x, relation.table)) diff --git a/packages/core/src/eval.ts b/packages/core/src/eval.ts index e9af27b5..c753fe5d 100644 --- a/packages/core/src/eval.ts +++ b/packages/core/src/eval.ts @@ -8,6 +8,8 @@ export function isEvalExpr(value: any): value is Eval.Expr { return value && Object.keys(value).some(key => key.startsWith('$')) } +export const isUpdateExpr: (value: any) => boolean = isEvalExpr + export function isAggrExpr(expr: Eval.Expr): boolean { return expr['$'] || expr['$select'] } From 78fb84e035a414f51d69e1e5f2acc615dfaab898 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Mon, 3 Jun 2024 22:14:08 +0800 Subject: [PATCH 13/17] chore: clean --- packages/core/src/driver.ts | 5 ----- packages/core/src/model.ts | 15 +-------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 31edc83d..0b56746c 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -28,11 +28,6 @@ export namespace Driver { include?: Relation.Include } - export interface CreateOptions { - upsert?: boolean - include?: any - } - export interface WriteResult { inserted?: number matched?: number diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index 444435a8..b95dcda1 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -1,7 +1,7 @@ import { clone, filterKeys, isNullable, makeArray, mapValues, MaybeArray } from 'cosmokit' import { Context } from 'cordis' import { Eval, Update } from './eval.ts' -import { DeepPartial, FlatKeys, Flatten, isFlat, Keys, Row, unravel, Values } from './utils.ts' +import { DeepPartial, FlatKeys, Flatten, isFlat, Keys, Row, unravel } from './utils.ts' import { Type } from './type.ts' import { Driver } from './driver.ts' import { Query } from './query.ts' @@ -36,19 +36,6 @@ export namespace Relation { shared?: MaybeArray | Record } - export interface Options = Keys> { - many: boolean - table: K - field?: Keys> | Keys[]> - ref?: MaybeArray> - nullable?: boolean - } - - export interface Extra { - shared?: string[] - extra?: object - } - export type Include = boolean | { [P in keyof T]?: T[P] extends MaybeArray | undefined ? U extends S ? Include : never : never } From 204e28ad5da20fffe3f41def60eddaf2914aa473 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Tue, 4 Jun 2024 15:43:46 +0800 Subject: [PATCH 14/17] fix: typing for database.extend --- packages/core/src/database.ts | 4 ++-- packages/core/src/model.ts | 6 +++--- packages/tests/src/relation.ts | 12 ++++-------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 5603f2f5..0985f8d8 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -117,7 +117,7 @@ export class Database extends Servi await driver.prepare(name) } - extend, T extends Field.Extension>(name: K, fields: T, config: Partial>> = {}) { + extend>(name: K, fields: Field.Extension, config: Partial>> = {}) { let model = this.tables[name] if (!model) { model = this.tables[name] = new Model(name) @@ -169,7 +169,7 @@ export class Database extends Servi references: [...Object.values(relation.shared), ...relation.references], }, } as any, { - primary: [...shared, ...fields, ...references].map(x => x[0]), + primary: [...shared, ...fields, ...references].map(x => x[0]) as any, }) } }) diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index b95dcda1..ec1e230e 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -33,7 +33,7 @@ export namespace Relation { target?: string references?: MaybeArray fields?: MaybeArray - shared?: MaybeArray | Record + shared?: MaybeArray | Partial> } export type Include = boolean | { @@ -78,7 +78,7 @@ export namespace Relation { type: def.type, table: def.table ?? relmodel.name, fields: makeArray(fields), - shared, + shared: shared as any, references: makeArray(def.references ?? relmodel.primary), required: def.type !== 'manyToOne' && model.name !== relmodel.name && makeArray(fields).every(key => !model.fields[key]?.nullable || makeArray(model.primary).includes(key)), @@ -177,7 +177,7 @@ export namespace Field { | Literal | Definition | Transform - | (O[K] extends object ? Relation.Definition> : never) + | (O[K] extends object | undefined ? Relation.Definition> : never) } export type Extension = MapField, N> diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index d3211fd4..515eabf7 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -1,4 +1,4 @@ -import { $, Database, Query, Relation, Values } from 'minato' +import { $, Database, Relation } from 'minato' import { expect } from 'chai' import { setup } from './utils' @@ -39,11 +39,7 @@ interface Post2Tag { tag?: Tag & { id: number } } -interface GuildSyncRef { - syncAt: number -} - -interface Login extends GuildSyncRef { +interface Login { id: string platform: string name?: string @@ -51,7 +47,7 @@ interface Login extends GuildSyncRef { syncs?: GuildSync[] } -interface Guild extends GuildSyncRef { +interface Guild { id: string platform2: string name?: string @@ -60,6 +56,7 @@ interface Guild extends GuildSyncRef { } interface GuildSync { + platform: string syncAt?: number guild?: Guild login?: Login @@ -126,7 +123,6 @@ function RelationTests(database: Database) { }) database.extend('post2tag', { - id: 'unsigned', 'post.id': 'unsigned', 'tag.id': 'unsigned', post: { From 7814a65a93f4a2fdc180e44d3a5671fecb264f33 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Tue, 4 Jun 2024 20:04:29 +0800 Subject: [PATCH 15/17] fix: typo, deduplicate relation primary --- packages/core/src/database.ts | 4 ++-- packages/tests/src/relation.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 0985f8d8..03b5de86 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -1,4 +1,4 @@ -import { defineProperty, Dict, filterKeys, makeArray, mapValues, MaybeArray, noop, omit, pick } from 'cosmokit' +import { deduplicate, defineProperty, Dict, filterKeys, makeArray, mapValues, MaybeArray, noop, omit, pick } from 'cosmokit' import { Context, Service, Spread } from 'cordis' import { AtomicTypes, DeepPartial, FlatKeys, FlatPick, Flatten, getCell, Indexable, Keys, randomId, Row, unravel, Values } from './utils.ts' import { Selection } from './selection.ts' @@ -175,7 +175,7 @@ export class Database extends Servi }) // use relation field as primary if (Array.isArray(model.primary) && model.primary.every(key => model.fields[key]?.relation)) { - model.primary = model.primary.map(key => model.fields[key]!.relation!.fields).flat() + model.primary = deduplicate(model.primary.map(key => model.fields[key]!.relation!.fields).flat()) } this.prepareTasks[name] = this.prepare(name) ;(this.ctx as Context).emit('model', name) diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index 515eabf7..3f3f6683 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -179,7 +179,7 @@ function RelationTests(database: Database) { fields: ['login.id', 'platform'], }, }, { - primary: ['platform', 'guild.id', 'login.id'], + primary: ['guild', 'login'], }) async function setupAutoInc(database: Database, name: K, length: number) { @@ -605,7 +605,7 @@ namespace RelationTests { ]) }) - it('manyToOne fallbacck', async () => { + it('manyToOne fallback', async () => { await setup(database, 'user', []) await setup(database, 'profile', []) await setup(database, 'post', []) From 47c70fe10a834031c8ee52c3597cd97f06d4b806 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Tue, 4 Jun 2024 20:23:18 +0800 Subject: [PATCH 16/17] chore: add test --- packages/tests/src/relation.ts | 95 +++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 8 deletions(-) diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index 3f3f6683..cff14b54 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -1001,23 +1001,40 @@ namespace RelationTests { it('explicit manyToMany', async () => { await setup(database, 'login', []) - await setup(database, 'guild', [ - { id: '1', platform2: 'sandbox', name: 'Guild1' }, + await setup(database, 'guild', []) + await setup(database, 'guildSync', []) + + await database.create('login', { + id: '1', + platform: 'sandbox', + name: 'Bot1', + syncs: { + $create: [ + { + syncAt: 123, + guild: { + $upsert: { id: '1', platform2: 'sandbox', name: 'Guild1' }, + }, + }, + ], + }, + }) + + await database.upsert('guild', [ { id: '2', platform2: 'sandbox', name: 'Guild2' }, { id: '3', platform2: 'sandbox', name: 'Guild3' }, ]) - await setup(database, Relation.buildAssociationTable('login', 'guild') as any, []) await database.create('login', { - id: '1', + id: '2', platform: 'sandbox', - name: 'Bot1', + name: 'Bot2', syncs: { $create: [ { syncAt: 123, guild: { - $connect: { id: '1' }, + $connect: { id: '2' }, }, }, ], @@ -1025,11 +1042,33 @@ namespace RelationTests { }) await expect(database.get('login', { - id: '1', platform: 'sandbox', }, { include: { syncs: { guild: true } }, - })).to.eventually.have.nested.property('[0].syncs').with.length(1) + })).to.eventually.have.shape([ + { + id: '1', + platform: 'sandbox', + name: 'Bot1', + syncs: [ + { + syncAt: 123, + guild: { id: '1', platform2: 'sandbox', name: 'Guild1' }, + }, + ], + }, + { + id: '2', + platform: 'sandbox', + name: 'Bot2', + syncs: [ + { + syncAt: 123, + guild: { id: '2', platform2: 'sandbox', name: 'Guild2' }, + }, + ], + }, + ]) }) } @@ -1585,6 +1624,46 @@ namespace RelationTests { { id: '4', logins: [{ id: '1' }, { id: '2' }] }, ]) }) + + it('explicit manyToMany', async () => { + await setup(database, 'login', [ + { id: '1', platform: 'sandbox', name: 'Guild1' }, + { id: '2', platform: 'sandbox', name: 'Guild2' }, + { id: '3', platform: 'sandbox', name: 'Guild3' }, + ]) + await setup(database, 'guild', []) + await setup(database, 'guildSync', []) + + await database.set('login', { + id: '1', + platform: 'sandbox', + }, { + syncs: { + $create: [ + { + syncAt: 123, + guild: { + $create: { id: '1', platform2: 'sandbox' }, + }, + }, + ], + }, + }) + + await expect(database.get('login', { + id: '1', + platform: 'sandbox', + }, { + include: { syncs: { guild: true } }, + })).to.eventually.have.shape([ + { + id: '1', + syncs: [ + { syncAt: 123, guild: { id: '1', platform2: 'sandbox' } }, + ], + }, + ]) + }) } } From b785ee7a760d63383e0f246b7d63a025d1da1c18 Mon Sep 17 00:00:00 2001 From: Hieuzest Date: Wed, 5 Jun 2024 00:19:15 +0800 Subject: [PATCH 17/17] feat: adjust oneToOne required --- packages/core/src/database.ts | 3 ++- packages/core/src/model.ts | 7 +++--- packages/tests/src/relation.ts | 43 ++++++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/core/src/database.ts b/packages/core/src/database.ts index 03b5de86..5d1e7d79 100644 --- a/packages/core/src/database.ts +++ b/packages/core/src/database.ts @@ -133,7 +133,8 @@ export class Database extends Servi } Object.entries(fields).forEach(([key, def]: [string, Relation.Definition]) => { if (!Relation.Type.includes(def.type)) return - const [relation, inverse] = Relation.parse(def, key, model, this.tables[def.table ?? key]) + const subprimary = !def.fields && makeArray(model.primary).includes(key) + const [relation, inverse] = Relation.parse(def, key, model, this.tables[def.table ?? key], subprimary) if (!this.tables[relation.table]) throw new Error(`relation table ${relation.table} does not exist`) ;(model.fields[key] = Field.parse('expr')).relation = relation if (def.target) { diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts index ec1e230e..583f2893 100644 --- a/packages/core/src/model.ts +++ b/packages/core/src/model.ts @@ -66,12 +66,12 @@ export namespace Relation { return [field, reference].sort().join('_') } - export function parse(def: Definition, key: string, model: Model, relmodel: Model): [Config, Config] { + export function parse(def: Definition, key: string, model: Model, relmodel: Model, subprimary?: boolean): [Config, Config] { const shared = !def.shared ? {} : typeof def.shared === 'string' ? { [def.shared]: def.shared } : Array.isArray(def.shared) ? Object.fromEntries(def.shared.map(x => [x, x])) : def.shared - const fields = def.fields ?? ((model.name === relmodel.name || def.type === 'manyToOne' + const fields = def.fields ?? ((subprimary || model.name === relmodel.name || def.type === 'manyToOne' || (def.type === 'oneToOne' && !makeArray(relmodel.primary).every(key => !relmodel.fields[key]?.nullable))) ? makeArray(relmodel.primary).map(x => `${key}.${x}`) : model.primary) const relation: Config = { @@ -96,9 +96,10 @@ export namespace Relation { fields: relation.references, references: relation.fields, shared: Object.fromEntries(Object.entries(shared).map(([k, v]) => [v, k])), - required: relation.type !== 'oneToMany' && !relation.required + required: relation.type !== 'oneToMany' && relation.references.every(key => !relmodel.fields[key]?.nullable || makeArray(relmodel.primary).includes(key)), } + if (inverse.required) relation.required = false return [relation, inverse] } } diff --git a/packages/tests/src/relation.ts b/packages/tests/src/relation.ts index cff14b54..d8f1afa6 100644 --- a/packages/tests/src/relation.ts +++ b/packages/tests/src/relation.ts @@ -74,11 +74,6 @@ interface Tables { } function RelationTests(database: Database) { - database.extend('profile', { - id: 'unsigned', - name: 'string', - }) - database.extend('user', { id: 'unsigned', value: 'integer', @@ -87,15 +82,20 @@ function RelationTests(database: Database) { table: 'user', target: 'predecessor', }, - profile: { - type: 'oneToOne', - table: 'profile', - target: 'user', - }, }, { autoInc: true, }) + database.extend('profile', { + id: 'unsigned', + name: 'string', + user: { + type: 'oneToOne', + table: 'user', + target: 'profile', + }, + }) + database.extend('post', { id2: 'unsigned', score: 'unsigned', @@ -164,8 +164,6 @@ function RelationTests(database: Database) { database.extend('guildSync', { syncAt: 'unsigned', platform: 'string', - 'guild.id': 'string', - 'login.id': 'string', guild: { type: 'manyToOne', table: 'guild', @@ -1235,6 +1233,27 @@ namespace RelationTests { ]) }) + nullableComparator && it('set null on oneToOne', async () => { + await setup(database, 'user', [ + { id: 1, value: 1, profile: { name: 'A' } }, + { id: 2, value: 2, profile: { name: 'B' } }, + { id: 3, value: 3, profile: { name: 'B' } }, + ] as any) + + await database.set('user', 1, { + profile: null, + }) + await expect(database.select('user', {}, { profile: true }).execute()).to.eventually.have.shape([ + { id: 1, value: 1, profile: null }, + { id: 2, value: 2, profile: { name: 'B' } }, + { id: 3, value: 3, profile: { name: 'B' } }, + ]) + + await expect(database.set('profile', 3, { + user: null, + })).to.be.eventually.rejected + }) + nullableComparator && it('manyToOne expr', async () => { await setup(database, 'user', []) await setup(database, 'profile', [])