Skip to content

Commit

Permalink
feat(core): support new orm api
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Mar 24, 2021
1 parent 6ef52a5 commit 256a040
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 109 deletions.
40 changes: 32 additions & 8 deletions packages/koishi-core/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,35 @@ export interface Tables {
channel: Channel
}

export namespace Tables {
type IndexType = string | number
type IndexKeys<O, T = any> = string & { [K in keyof O]: O[K] extends T ? K : never }[keyof O]
type QueryMap<O> = { [K in keyof O]?: O[K][] }
export type Index<T extends TableType> = IndexKeys<Tables[T], IndexType>
export type Query<T extends TableType> = IndexType[] | QueryMap<Tables[T]>
export type Field<T extends TableType> = string & keyof Tables[T]

interface Meta<O> {
primary?: keyof O
type?: 'incremental'
}

export const config: { [T in TableType]?: Meta<Tables[T]> } = {}

export function extend<T extends TableType>(name: T, meta?: Meta<Tables[T]>) {
config[name] = { primary: 'id', type: 'incremental', ...meta } as any
}

extend('user')
extend('channel')

export function resolveQuery<T extends TableType>(name: T, query: Query<T>): Record<string, any[]> {
if (!Array.isArray(query)) return query
const { primary } = config[name]
return { [primary]: query }
}
}

export interface User extends Record<Platform, string> {
id: string
flag: number
Expand Down Expand Up @@ -86,16 +115,12 @@ export namespace Channel {
}

type MaybeArray<T> = T | T[]
type IndexKeys<O, T = any> = string & { [K in keyof O]: O[K] extends T ? K : never }[keyof O]
type TableIndex<T extends TableType> = IndexKeys<Tables[T], string | number>

/* eslint-disable max-len */
export interface Database {
get<T extends TableType, K extends TableIndex<T>, F extends string & keyof Tables[T]>(table: T, key: K, value: Tables[T][K][], fields?: readonly F[]): Promise<Pick<Tables[T], F>[]>
getAll<T extends TableType, F extends string & keyof Tables[T]>(table: T, fields?: readonly F[]): Promise<Pick<Tables[T], F>[]>
get<T extends TableType, F extends Tables.Field<T>>(table: T, query: Tables.Query<T>, fields?: readonly F[]): Promise<Pick<Tables[T], F>[]>
remove<T extends TableType>(table: T, query: Tables.Query<T>): Promise<void>
create<T extends TableType>(table: T, data: Partial<Tables[T]>): Promise<Tables[T]>
update<T extends TableType>(table: T, data: Partial<Tables[T]>[], key?: TableIndex<T>): Promise<void>
remove<T extends TableType, K extends TableIndex<T>>(table: T, key: K, value: Tables[T][K][]): Promise<void>
update<T extends TableType>(table: T, data: Partial<Tables[T]>[], key?: Tables.Index<T>): Promise<void>

getUser<K extends User.Field, T extends User.Index>(type: T, id: string, fields?: readonly K[]): Promise<Pick<User, K | T>>
getUser<K extends User.Field, T extends User.Index>(type: T, ids: readonly string[], fields?: readonly K[]): Promise<Pick<User, K | T>[]>
Expand All @@ -111,7 +136,6 @@ export interface Database {
createChannel(type: Platform, id: string, data: Partial<Channel>): Promise<void>
removeChannel(type: Platform, id: string): Promise<void>
}
/* eslint-enable max-len */

type Methods<S, T> = {
[K in keyof S]?: S[K] extends (...args: infer R) => infer U ? (this: T, ...args: R) => U : S[K]
Expand Down
29 changes: 14 additions & 15 deletions packages/koishi-test-utils/src/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,20 @@ export class MemoryDatabase {
}

Database.extend(MemoryDatabase, {
async get(table, key, values, fields) {
return this.$table<any>(table)
.filter(row => values.includes(row[key]))
async get(name, query, fields) {
const entries = Object.entries(Tables.resolveQuery(name, query))
return this.$table(name)
.filter(row => entries.every(([key, value]) => value.includes(row[key])))
.map(row => fields ? pick(row, fields) : row)
.map(clone)
},

async remove(name, query) {
const entries = Object.entries(Tables.resolveQuery(name, query))
this.$store[name] = this.$table(name)
.filter(row => !entries.every(([key, value]) => value.includes(row[key])))
},

async create(table, data: any) {
const store = this.$table(table)
const { primary = 'id' } = MemoryDatabase.tables[table] || {}
Expand All @@ -63,14 +70,6 @@ Database.extend(MemoryDatabase, {
return data
},

async remove(table, key, values) {
const store = this.$table(table)
for (const id of values) {
const index = store.findIndex(row => row[key] === id)
if (index >= 0) store.splice(index, 1)
}
},

async update(table, data, key: string) {
if (key) key = (MemoryDatabase.tables[table] || {}).primary || 'id'
for (const item of data) {
Expand All @@ -81,9 +80,9 @@ Database.extend(MemoryDatabase, {

async getUser(type, id, fields) {
if (Array.isArray(id)) {
return this.get('user', type, id, fields) as any
return this.get('user', { [type]: id }, fields) as any
} else {
return (await this.get('user', type as any, [id], fields))[0]
return (await this.get('user', { [type]: [id] }, fields))[0]
}
},

Expand Down Expand Up @@ -117,9 +116,9 @@ Database.extend(MemoryDatabase, {

async getChannel(type, id, fields) {
if (Array.isArray(id)) {
return this.get('channel', 'id', id.map(id => `${type}:${id}`), fields)
return this.get('channel', id.map(id => `${type}:${id}`), fields)
} else {
return (await this.get('channel', 'id', [`${type}:${id}`], fields))[0]
return (await this.get('channel', [`${type}:${id}`], fields))[0]
}
},

Expand Down
6 changes: 4 additions & 2 deletions packages/koishi-test-utils/tests/memory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai'
import { Database } from 'koishi-core'
import { Database, Tables } from 'koishi-core'
import { testDatabase, App } from 'koishi-test-utils'

declare module 'koishi-core' {
Expand All @@ -19,13 +19,15 @@ interface FooData {
bar: string
}

Tables.extend('foo')

Database.extend('koishi-test-utils', {
async createFoo(data: FooData) {
return this.create('foo', data)
},

async removeFoo(id: number) {
return this.remove('foo', 'id', [id])
return this.remove('foo', [id])
},

async getFooCount() {
Expand Down
10 changes: 5 additions & 5 deletions packages/plugin-github/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function authorize(ctx: Context, config: Config) {
}

const url = `https://api.github.com/repos/${name}/hooks`
const [repo] = await ctx.database.get('github', 'name', [name])
const [repo] = await ctx.database.get('github', [name])
if (options.add) {
if (repo) return `已经添加过仓库 ${name}。`
const secret = Random.uuid()
Expand All @@ -82,14 +82,14 @@ function authorize(ctx: Context, config: Config) {
await ctx.database.create('github', { name, id, secret })
return '添加仓库成功!'
} else {
const [repo] = await ctx.database.get('github', 'name', [name])
const [repo] = await ctx.database.get('github', [name])
if (!repo) return `尚未添加过仓库 ${name}。`
await ctx.app.github.request(`${url}/${repo.id}`, 'DELETE', session)
return '移除仓库成功!'
}
}

const repos = await ctx.database.getAll('github')
const repos = await ctx.database.get('github', {})
if (!repos.length) return '当前没有监听的仓库。'
return repos.map(repo => repo.name).join('\n')
})
Expand All @@ -111,7 +111,7 @@ function authorize(ctx: Context, config: Config) {
if (!session.channel) return '当前不是群聊上下文。'
if (!name) return '请输入仓库名。'
if (!/^[\w-]+\/[\w-]+$/.test(name)) return '请输入正确的仓库名。'
const [repo] = await ctx.database.get('github', 'name', [name])
const [repo] = await ctx.database.get('github', [name])
if (!repo) return `尚未添加过仓库 ${name}。`

const webhooks = session.channel.githubWebhooks
Expand Down Expand Up @@ -160,7 +160,7 @@ export function apply(ctx: Context, config: Config = {}) {

async function getSecret(name: string) {
if (!ctx.database) return config.repos.find(repo => repo.name === name)?.secret
const [data] = await ctx.database.get('github', 'name', [name])
const [data] = await ctx.database.get('github', [name])
return data?.secret
}

Expand Down
8 changes: 3 additions & 5 deletions packages/plugin-github/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { EventConfig } from './events'
import axios, { AxiosError, Method } from 'axios'
import { App, Channel, Database, Session, User } from 'koishi-core'
import { App, Channel, Database, Session, Tables, User } from 'koishi-core'
import { segment, Logger } from 'koishi-utils'
import {} from 'koishi-plugin-puppeteer'
import {} from 'koishi-plugin-mysql'
Expand Down Expand Up @@ -36,6 +36,8 @@ Channel.extend(() => ({
githubWebhooks: {},
}))

Tables.extend('github', { primary: 'name' })

Database.extend('koishi-plugin-mysql', ({ tables, Domain }) => {
tables.user.ghAccessToken = 'varchar(50)'
tables.user.ghRefreshToken = 'varchar(50)'
Expand All @@ -47,10 +49,6 @@ Database.extend('koishi-plugin-mysql', ({ tables, Domain }) => {
})
})

Database.extend('koishi-plugin-mongo', ({ tables }) => {
tables.github = { primary: 'name' }
})

interface Repository {
name: string
secret: string
Expand Down
15 changes: 0 additions & 15 deletions packages/plugin-mongo/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ export interface Config {
uri?: string
}

interface TableConfig<O> {
primary?: keyof O
type?: 'incremental'
}

export interface MongoDatabase extends Database {}

export class MongoDatabase {
Expand All @@ -39,8 +34,6 @@ export class MongoDatabase {
user: Collection<User>
channel: Collection<Channel>

static readonly tables: { [T in TableType]?: TableConfig<Tables[T]> } = {}

constructor(public app: App, config?: Config) {
this.config = {
host: 'localhost',
Expand Down Expand Up @@ -71,14 +64,6 @@ export class MongoDatabase {
return this.db.collection(name)
}

getConfig<T extends TableType>(name: T): TableConfig<Tables[T]> {
return {
primary: 'id',
type: 'incremental',
...MongoDatabase.tables[name],
}
}

stop() {
return this.client.close()
}
Expand Down
63 changes: 32 additions & 31 deletions packages/plugin-mongo/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import MongoDatabase, { Config } from './database'
import { User, Database, Context, Channel, Random, pick, omit } from 'koishi-core'
import { User, Tables, Database, Context, Channel, Random, pick, omit, TableType } from 'koishi-core'

export * from './database'
export default MongoDatabase
Expand Down Expand Up @@ -76,58 +76,59 @@ function unescapeKey<T extends Partial<User>>(data: T) {
return data
}

Database.extend(MongoDatabase, ({ tables }) => {
tables.user = { primary: 'id' }
tables.channel = { primary: 'id' }
})
function createFilter<T extends TableType>(name: T, _query: Tables.Query<T>) {
const query = Tables.resolveQuery(name, _query)
const filter = {}
for (const key in query) {
const value = query[key]
if (!value.length) return
filter[key] = { $in: value }
}
const { primary } = Tables.config[name]
if (filter[primary]) {
filter['_id'] = filter[primary]
delete filter[primary]
}
return filter
}

Database.extend(MongoDatabase, {
async getAll(table, fields) {
const { primary } = this.getConfig(table)
let cursor = this.db.collection(table).find()
async get(name, query, fields) {
const filter = createFilter(name, query)
if (!filter) return []
let cursor = this.db.collection(name).find(filter)
if (fields) cursor = cursor.project(projection(fields))
const data = await cursor.toArray()
const { primary } = Tables.config[name]
for (const item of data) item[primary] = item._id
return data
},

async get(table, key, value, fields) {
if (!value.length) return []
const { primary } = this.getConfig(table)
if (key === primary) key = '_id'
let cursor = this.db.collection(table).find({ [key]: { $in: value } })
if (fields) cursor = cursor.project(projection(fields))
const data = await cursor.toArray()
for (const item of data) item[primary] = item._id
return data
async remove(name, query) {
const filter = createFilter(name, query)
if (!filter) return
await this.db.collection(name).deleteMany(filter)
},

async create(table, data: any) {
const { primary, type } = this.getConfig(table)
async create(name, data: any) {
const { primary, type } = Tables.config[name]
const copy = { ...data }
if (copy[primary]) {
copy['_id'] = copy[primary]
delete copy[primary]
} else if (type === 'incremental') {
const [latest] = await this.db.collection(table).find().sort('_id', -1).limit(1).toArray()
const [latest] = await this.db.collection(name).find().sort('_id', -1).limit(1).toArray()
copy['_id'] = data[primary] = latest ? latest._id + 1 : 1
}
await this.db.collection(table).insertOne(copy)
await this.db.collection(name).insertOne(copy)
return data
},

async remove(table, key, value) {
if (!value.length) return
const { primary } = this.getConfig(table)
if (key === primary) key = '_id'
await this.db.collection(table).deleteMany({ [key]: { $in: value } })
},

async update(table, data: any[], key: string) {
async update(name, data: any[], key: string) {
if (!data.length) return
const { primary } = this.getConfig(table)
const { primary } = Tables.config[name]
if (!key || key === primary) key = '_id'
const bulk = this.db.collection(table).initializeUnorderedBulkOp()
const bulk = this.db.collection(name).initializeUnorderedBulkOp()
for (const item of data) {
bulk.find({ [key]: data[primary] }).updateOne({ $set: omit(item, [primary]) })
}
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin-monitor/src/database.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {} from 'koishi-plugin-mysql'
import {} from 'koishi-plugin-mongo'
import { Channel, Database } from 'koishi-core'
import { Channel, Database, Tables } from 'koishi-core'
import { OkPacket } from 'mysql'

declare module 'koishi-core' {
Expand Down Expand Up @@ -45,6 +45,8 @@ const subscribeKeys = [
'twitCasting', 'twitCastingStatus',
] as SubscribeField[]

Tables.extend('subscribe')

Database.extend('koishi-plugin-mysql', {
async getSubscribes(ids, keys = subscribeKeys) {
if (!ids) return this.query('SELECT * FROM `subscribe`')
Expand Down

0 comments on commit 256a040

Please sign in to comment.