Skip to content

Commit

Permalink
feat(core): ORM API (#179)
Browse files Browse the repository at this point in the history
* fix(utils): handle undefined & null for clone()
* fix(core): collect global command fields for every encountered argv
* feat(utils): support clone date & regexp objects
* feat(utils): format time interval by templates

also modified: core teach monitor schedule mysql mongo test-utils utils
  • Loading branch information
shigma committed Mar 23, 2021
1 parent 8172dc7 commit f2e5535
Show file tree
Hide file tree
Showing 31 changed files with 367 additions and 313 deletions.
1 change: 1 addition & 0 deletions .mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const specs = [
'packages/plugin-common/tests/*.spec.ts',
'packages/plugin-eval/tests/*.spec.ts',
'packages/plugin-github/tests/*.spec.ts',
'packages/plugin-schedule/tests/*.spec.ts',
'packages/plugin-teach/tests/*.spec.ts',
]

Expand Down
11 changes: 10 additions & 1 deletion packages/koishi-core/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,17 @@ export namespace Channel {
extend((type, id) => ({ id: `${type}:${id}`, flag: 0, disable: [] }))
}

type MaybeArray<T> = T | readonly T[]
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>[]>
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>

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>[]>
setUser<T extends User.Index>(type: T, id: string, data: Partial<User>): Promise<void>
Expand All @@ -102,6 +110,7 @@ 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
4 changes: 2 additions & 2 deletions packages/koishi-core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class Session<
defineProperty(this, '_hooks', [])
}

toJSON() {
toJSON(): Partial<Session> {
return Object.fromEntries(Object.entries(this).filter(([key]) => {
return !key.startsWith('_') && !key.startsWith('$')
}))
Expand Down Expand Up @@ -285,7 +285,6 @@ export class Session<
}

collect<T extends TableType>(key: T, argv: Argv, fields = new Set<keyof Tables[T]>()) {
collectFields(argv, Command[`_${key}Fields`], fields)
const collect = (argv: Argv) => {
argv.session = this
if (argv.tokens) {
Expand All @@ -294,6 +293,7 @@ export class Session<
}
}
if (!this.resolve(argv)) return
collectFields(argv, Command[`_${key}Fields`], fields)
collectFields(argv, argv.command[`_${key}Fields`], fields)
}
collect(argv)
Expand Down
12 changes: 7 additions & 5 deletions packages/koishi-test-utils/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,13 @@ export class MockedApp extends App {
return this.bots[0].selfId
}

async start() {
this.status = App.Status.open
this.emit('connect')
}

receive(meta: Partial<Session>) {
const session = new Session(this, {
platform: 'mock',
selfId: this.selfId,
...meta,
})
const session = new Session(this, meta)
this.adapters.mock.dispatch(session)
return session.id
}
Expand All @@ -132,6 +133,7 @@ export class TestSession {
this.meta = {
platform: 'mock',
type: 'message',
selfId: app.selfId,
userId,
author: {
userId,
Expand Down
78 changes: 48 additions & 30 deletions packages/koishi-test-utils/src/memory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Tables, TableType, App, Database, User, Channel } from 'koishi-core'
import { clone } from 'koishi-utils'
import { clone, pick } from 'koishi-utils'

declare module 'koishi-core' {
interface Database {
Expand All @@ -19,6 +19,10 @@ export interface MemoryConfig {}

export interface MemoryDatabase extends Database {}

interface TableConfig<O> {
primary?: keyof O
}

export class MemoryDatabase {
$store: { [K in TableType]?: Tables[K][] } = {
user: [],
Expand All @@ -27,46 +31,59 @@ export class MemoryDatabase {

memory = this

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

constructor(public app: App, public config: MemoryConfig) {}

$table<K extends TableType>(table: K): Tables[K][] {
return this.$store[table] as any
$table<K extends TableType>(table: K): any[] {
return this.$store[table]
}

$select<T extends TableType, K extends keyof Tables[T]>(table: T, key: K, values: readonly Tables[T][K][]) {
return this.$table(table).filter(row => values.includes(row[key])).map(clone)
$count<K extends TableType>(table: K, field: keyof Tables[K] = 'id') {
return new Set(this.$table(table).map(data => data[field])).size
}
}

$create<K extends TableType>(table: K, data: Tables[K]) {
Database.extend(MemoryDatabase, {
async get(table, key, values, fields) {
return this.$table<any>(table)
.filter(row => values.includes(row[key]))
.map(row => fields ? pick(row, fields) : row)
.map(clone)
},

async create(table, data: any) {
const store = this.$table(table)
const max = store.length ? Math.max(...store.map(row => +row.id)) : 0
data.id = max + 1 as any
const { primary = 'id' } = MemoryDatabase.tables[table] || {}
if (!data[primary]) {
const max = store.length ? Math.max(...store.map(row => +row[primary])) : 0
data[primary] = max + 1
}
store.push(data)
return data
}
},

$remove<K extends TableType>(table: K, id: number) {
async remove(table, key, values) {
const store = this.$table(table)
const index = store.findIndex(row => +row.id === id)
if (index >= 0) store.splice(index, 1)
}

$update<K extends TableType>(table: K, id: number, data: Partial<Tables[K]>) {
const row = this.$table(table).find(row => +row.id === id)
Object.assign(row, clone(data))
}
for (const id of values) {
const index = store.findIndex(row => row[key] === id)
if (index >= 0) store.splice(index, 1)
}
},

$count<K extends TableType>(table: K, field: keyof Tables[K] = 'id') {
return new Set(this.$table(table).map(data => data[field])).size
}
}
async update(table, data, key: string) {
if (key) key = (MemoryDatabase.tables[table] || {}).primary || 'id'
for (const item of data) {
const row = this.$table(table).find(row => row[key] === item[key])
Object.assign(row, clone(item))
}
},

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

Expand All @@ -87,21 +104,22 @@ Database.extend(MemoryDatabase, {
const table = this.$table('user')
const index = table.findIndex(row => row[type] === id)
if (index >= 0) return
this.$create('user', {
const user = await this.create('user', {
...User.create(type, id),
...clone(data),
})
user.id = '' + user.id
},

initUser(id, authority = 1) {
return this.createUser('mock', id, { authority })
},

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

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

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

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

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

async getFooCount() {
Expand Down
23 changes: 20 additions & 3 deletions packages/koishi-utils/src/misc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { types } from 'util'

export function noop(): any {}

export function isInteger(source: any) {
Expand All @@ -20,9 +22,24 @@ export function defineEnumProperty<T extends object>(object: T, key: keyof T, va
const primitives = ['number', 'string', 'bigint', 'boolean', 'symbol']

export function clone<T extends unknown>(source: T): T {
return primitives.includes(typeof source) ? source
: Array.isArray(source) ? source.map(clone) as any
: Object.fromEntries(Object.entries(source).map(([key, value]) => [key, clone(value)]))
// primitive types
if (primitives.includes(typeof source)) return source

// null & undefined
if (!source) return source

// array
if (Array.isArray(source)) return source.map(clone) as any

// date
if (types.isDate(source)) return new Date(source.valueOf()) as any

// regexp
if (types.isRegExp(source)) return new RegExp(source.source, source.flags) as any

// fallback
const entries = Object.entries(source).map(([key, value]) => [key, clone(value)])
return Object.fromEntries(entries)
}

export function merge<T extends object>(head: T, base: T): T {
Expand Down
4 changes: 2 additions & 2 deletions packages/koishi-utils/src/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ export namespace Time {

export function formatTimeInterval(time: Date, interval?: number) {
if (!interval) {
return time.toLocaleString()
return template('yyyy-MM-dd hh:mm:ss', time)
} else if (interval === day) {
return `每天 ${toHourMinute(time)}`
} else if (interval === week) {
return `每周${dayMap[time.getDay()]} ${toHourMinute(time)}`
} else {
return `${time.toLocaleString('zh-CN', { hour12: false })} 起每隔 ${formatTime(interval)}`
return `${template('yyyy-MM-dd hh:mm:ss', time)} 起每隔 ${formatTime(interval)}`
}
}
}
2 changes: 1 addition & 1 deletion packages/koishi-utils/tests/time.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('Time Manipulations', () => {
})

it('format time interval', () => {
expect(Time.formatTimeInterval(date)).to.equal(date.toLocaleString())
expect(Time.formatTimeInterval(date)).to.equal('2020-04-01 01:30:00')
expect(Time.formatTimeInterval(date, Time.day)).to.equal('每天 01:30')
Time.formatTimeInterval(date, Time.week) // make coverage happy
Time.formatTimeInterval(date, Time.hour) // make coverage happy
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-common/tests/handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,25 @@ function receive(session: Partial<Session>) {
}

const receiveFriendRequest = (userId: string) => receive({
platform: 'mock',
selfId: app.selfId,
type: 'friend-request',
messageId: 'flag',
userId,
})

const receiveGroupRequest = (userId: string) => receive({
platform: 'mock',
selfId: app.selfId,
type: 'group-request',
groupId: '10000',
messageId: 'flag',
userId,
})

const receiveGroupMemberRequest = (userId: string) => receive({
platform: 'mock',
selfId: app.selfId,
type: 'group-member-request',
groupId: '10000',
messageId: 'flag',
Expand Down
15 changes: 15 additions & 0 deletions packages/plugin-mongo/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface Config {
uri?: string
}

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

export interface MongoDatabase extends Database {}

export class MongoDatabase {
Expand All @@ -34,6 +39,8 @@ 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 @@ -64,6 +71,14 @@ 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

0 comments on commit f2e5535

Please sign in to comment.