@d-mok/quasar-app-extension-quasar-axe 0.0.94 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/boot/AutoRoute.ts +1 -1
- package/src/components/QxLayout.vue +3 -1
- package/src/templates/src/utils.ts +4 -2
- package/src/utils/ORM/basic.ts +228 -0
- package/src/utils/ORM/table.ts +155 -0
- package/src/utils/ORM/view.ts +94 -0
- package/src/utils/supabase.ts +28 -13
- package/src/utils/table.ts +0 -395
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-mok/quasar-app-extension-quasar-axe",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A Quasar App Extension",
|
|
5
5
|
"author": "d-mok <49301824+d-mok@users.noreply.github.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,18 +20,18 @@
|
|
|
20
20
|
"yarn": ">= 1.6.0"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@supabase/supabase-js": "^1.29.
|
|
24
|
-
"@types/papaparse": "^5.2
|
|
25
|
-
"@types/webpack-env": "^1.16.
|
|
23
|
+
"@supabase/supabase-js": "^1.29.4",
|
|
24
|
+
"@types/papaparse": "^5.3.2",
|
|
25
|
+
"@types/webpack-env": "^1.16.3",
|
|
26
26
|
"papaparse": "^5.3.1",
|
|
27
|
-
"sapphire-js": "^1.0.
|
|
27
|
+
"sapphire-js": "^1.0.63"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@quasar/app": "^3.
|
|
31
|
-
"@quasar/extras": "^1.12.
|
|
32
|
-
"@types/node": "^17.0.
|
|
33
|
-
"core-js": "^3.20.
|
|
34
|
-
"quasar": "^2.
|
|
35
|
-
"typescript": "^4.5.
|
|
30
|
+
"@quasar/app": "^3.3.1",
|
|
31
|
+
"@quasar/extras": "^1.12.4",
|
|
32
|
+
"@types/node": "^17.0.13",
|
|
33
|
+
"core-js": "^3.20.3",
|
|
34
|
+
"quasar": "^2.4.13",
|
|
35
|
+
"typescript": "^4.5.5"
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/boot/AutoRoute.ts
CHANGED
|
@@ -11,7 +11,7 @@ console.log("[QuasarAxe] Run Boot AutoRoute")
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
export default boot(({ app, router }) => {
|
|
14
|
-
const pages = require.context('src/pages',
|
|
14
|
+
const pages = require.context('src/pages', false, /[\w-]+\.vue$/)
|
|
15
15
|
router.addRoute({
|
|
16
16
|
path: '/',
|
|
17
17
|
//@ts-ignore
|
|
@@ -4,11 +4,13 @@ export { qDialog } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/dialog
|
|
|
4
4
|
//@ts-ignore
|
|
5
5
|
export { qNotify } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/notify'
|
|
6
6
|
//@ts-ignore
|
|
7
|
-
export { Table, Empower } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/table'
|
|
7
|
+
export { Table, Empower } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/ORM/table'
|
|
8
|
+
//@ts-ignore
|
|
9
|
+
export { TableView } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/ORM/view'
|
|
8
10
|
//@ts-ignore
|
|
9
11
|
import '@d-mok/quasar-app-extension-quasar-axe/src/utils/supabase'
|
|
10
12
|
//@ts-ignore
|
|
11
|
-
export { supabase, handleError } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/supabase'
|
|
13
|
+
export { supabase, handleError, callRPC } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/supabase'
|
|
12
14
|
//@ts-ignore
|
|
13
15
|
export { Sheet, List } from '@d-mok/quasar-app-extension-quasar-axe/src/utils/sapphire'
|
|
14
16
|
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { supabase, handleError, PostgrestFilterBuilder } from '../supabase'
|
|
2
|
+
import { qDialog } from '../dialog'
|
|
3
|
+
import { qNotify } from '../notify'
|
|
4
|
+
import { LoadingBar } from 'quasar'
|
|
5
|
+
import { Ordering, Criteria, strKeyOf, Sheet } from 'sapphire-js'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export type Where<T> =
|
|
9
|
+
{ [K in keyof T]?
|
|
10
|
+
: T[K]
|
|
11
|
+
| T[K][]
|
|
12
|
+
| { like: string }
|
|
13
|
+
}
|
|
14
|
+
| ((_: PostgrestFilterBuilder) => any)
|
|
15
|
+
|
|
16
|
+
// export type OrderBy<T> =
|
|
17
|
+
// strKeyOf<T>
|
|
18
|
+
// | { [K in keyof T]?: true | false }
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
type Action = 'select' | 'insert' | 'update' | 'delete' | 'patch'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
function loadingBar(on: boolean): void {
|
|
27
|
+
on ? setTimeout(() => LoadingBar.setDefaults({ size: "5px" }), 3000)
|
|
28
|
+
: LoadingBar.setDefaults({ size: "0px" })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A subclass of array designed as an ORM.
|
|
39
|
+
* @template T - type of element class
|
|
40
|
+
* @template R - type of database row class
|
|
41
|
+
*/
|
|
42
|
+
export abstract class BasicTable<T extends R, R extends object> extends Sheet<T>{
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The name of the database table.
|
|
46
|
+
*/
|
|
47
|
+
public abstract readonly tableName: string
|
|
48
|
+
/**
|
|
49
|
+
* The name of the ID field.
|
|
50
|
+
*/
|
|
51
|
+
public abstract readonly idField: strKeyOf<R>
|
|
52
|
+
/**
|
|
53
|
+
* The class of the elements.
|
|
54
|
+
*/
|
|
55
|
+
public abstract readonly entityClass: new (_: Partial<R>) => T
|
|
56
|
+
/**
|
|
57
|
+
* The default ordering instruction object.
|
|
58
|
+
*/
|
|
59
|
+
public readonly ordering: Readonly<Ordering<T>> = []
|
|
60
|
+
// /**
|
|
61
|
+
// * The default fields to select
|
|
62
|
+
// */
|
|
63
|
+
// public readonly selectFields: strKeyOf<R>[] = []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
private convert(data: R[]): T[] {
|
|
68
|
+
return data.map($ => new this.entityClass($))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private log(msg: string, data: T[]): void {
|
|
72
|
+
if (process.env.DEBUGGING)
|
|
73
|
+
console.log(`[${this.tableName}] ${msg}`, data)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
// private defaultOrder(): void {
|
|
79
|
+
// if (this.ordering !== undefined)
|
|
80
|
+
// this.order(...this.ordering)
|
|
81
|
+
// }
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
// private selectedFields() {
|
|
85
|
+
// return this.selectFields === undefined ? "*" : this.selectFields.join(",")
|
|
86
|
+
// }
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
protected idWhere(id: R[this['idField']]): any {
|
|
90
|
+
return { [this.idField]: id }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
protected idsWhere(ids: R[this['idField']][]): any {
|
|
95
|
+
return { [this.idField]: ids }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
protected orderById(ids: R[this['idField']][]): void {
|
|
100
|
+
let ord = { [this.idField]: ids } as any
|
|
101
|
+
this.order(ord)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
private APIAction(type: Action, content: Partial<R>[] = []) {
|
|
106
|
+
let q = supabase.from<R>(this.tableName)
|
|
107
|
+
switch (type) {
|
|
108
|
+
case 'select':
|
|
109
|
+
return q.select('*')
|
|
110
|
+
case 'patch':
|
|
111
|
+
return q.select('*')
|
|
112
|
+
case 'delete':
|
|
113
|
+
return q.delete()
|
|
114
|
+
case 'insert':
|
|
115
|
+
return q.insert(content)
|
|
116
|
+
case 'update':
|
|
117
|
+
return q.update(content[0])
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected async API({
|
|
122
|
+
type,
|
|
123
|
+
content,
|
|
124
|
+
where,
|
|
125
|
+
// orderBy,
|
|
126
|
+
// limit,
|
|
127
|
+
action = (_: any) => { },
|
|
128
|
+
confirm,
|
|
129
|
+
notify,
|
|
130
|
+
slience
|
|
131
|
+
}: {
|
|
132
|
+
type: Action
|
|
133
|
+
content?: Partial<R>[]
|
|
134
|
+
where?: Where<R>
|
|
135
|
+
// orderBy?: OrderBy<R>
|
|
136
|
+
// limit?: number,
|
|
137
|
+
action: (entities: T[]) => void
|
|
138
|
+
confirm?: [string, string?]
|
|
139
|
+
notify?: string
|
|
140
|
+
slience?: boolean
|
|
141
|
+
}) {
|
|
142
|
+
if (slience) loadingBar(false)
|
|
143
|
+
if (confirm) await qDialog.confirm(...confirm)
|
|
144
|
+
|
|
145
|
+
let q = this.APIAction(type, content)
|
|
146
|
+
q = applyWhere(q, where)
|
|
147
|
+
// q = applyOrderBy(q, orderBy)
|
|
148
|
+
// q = applyLimit(q, limit)
|
|
149
|
+
|
|
150
|
+
let { data, error } = await q
|
|
151
|
+
handleError(data, error)
|
|
152
|
+
|
|
153
|
+
let entities = this.convert(data)
|
|
154
|
+
action(entities)
|
|
155
|
+
this.order(...this.ordering)
|
|
156
|
+
|
|
157
|
+
this.log(type, entities)
|
|
158
|
+
if (notify) qNotify.toast(notify)
|
|
159
|
+
if (slience) loadingBar(true)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get an item by `criteria`. If not found, return a new object.
|
|
168
|
+
*/
|
|
169
|
+
got(criteria: Criteria<T>): T {
|
|
170
|
+
return this.get(criteria) ?? new this.entityClass({})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
function applyWhere(q: PostgrestFilterBuilder, where?: Where<any>): PostgrestFilterBuilder {
|
|
180
|
+
if (where === undefined)
|
|
181
|
+
return q
|
|
182
|
+
|
|
183
|
+
if (typeof where === 'function')
|
|
184
|
+
return where(q)
|
|
185
|
+
|
|
186
|
+
for (let key in where) {
|
|
187
|
+
let val = where[key]
|
|
188
|
+
if (Array.isArray(val)) {
|
|
189
|
+
// treat as in
|
|
190
|
+
q = q.in(key, val)
|
|
191
|
+
} else if (typeof val === 'object') {
|
|
192
|
+
if ('like' in val) {
|
|
193
|
+
// treat as like
|
|
194
|
+
let { like } = val as { like: string }
|
|
195
|
+
q = q.like(key, like)
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
// treat as eq
|
|
199
|
+
q = q.eq(key, val)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return q
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// function applyOrderBy(q: PostgrestFilterBuilder, orderBy?: OrderBy<any>): PostgrestFilterBuilder {
|
|
206
|
+
// if (orderBy === undefined)
|
|
207
|
+
// return q
|
|
208
|
+
|
|
209
|
+
// if (typeof orderBy === "string") {
|
|
210
|
+
// q = q.order(orderBy)
|
|
211
|
+
// } else {
|
|
212
|
+
// let field = Object.keys(orderBy)[0]
|
|
213
|
+
// let ascending = orderBy[field]
|
|
214
|
+
// q = q.order(field, { ascending })
|
|
215
|
+
// }
|
|
216
|
+
// return q
|
|
217
|
+
// }
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
// function applyLimit(q: PostgrestFilterBuilder, limit?: number): PostgrestFilterBuilder {
|
|
222
|
+
// if (limit === undefined)
|
|
223
|
+
// return q
|
|
224
|
+
|
|
225
|
+
// return q.limit(limit)
|
|
226
|
+
// }
|
|
227
|
+
|
|
228
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { supabase, handleError } from '../supabase'
|
|
2
|
+
import { Criteria, strKeyOf } from 'sapphire-js'
|
|
3
|
+
import { TableView } from './view'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type muteOptions = {
|
|
7
|
+
confirm?: [string, string?]
|
|
8
|
+
notify?: string
|
|
9
|
+
slience?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A subclass of array designed as an ORM.
|
|
16
|
+
* @template T - type of element class
|
|
17
|
+
* @template R - type of database row class
|
|
18
|
+
*/
|
|
19
|
+
export abstract class Table<T extends R, R extends object> extends TableView<T, R>{
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async insert(
|
|
25
|
+
rows: Partial<R> | Partial<R>[],
|
|
26
|
+
{ confirm, notify, slience }: muteOptions = {}
|
|
27
|
+
) {
|
|
28
|
+
await this.API({
|
|
29
|
+
type: 'insert',
|
|
30
|
+
content: Array.isArray(rows) ? rows : [rows],
|
|
31
|
+
action: entities => this.push(...entities),
|
|
32
|
+
confirm,
|
|
33
|
+
notify,
|
|
34
|
+
slience
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async update(
|
|
40
|
+
id: R[this['idField']],
|
|
41
|
+
row: Partial<R>,
|
|
42
|
+
{ confirm, notify, slience }: muteOptions = {}
|
|
43
|
+
) {
|
|
44
|
+
await this.API({
|
|
45
|
+
type: 'update',
|
|
46
|
+
content: [row],
|
|
47
|
+
where: this.idWhere(id),
|
|
48
|
+
action: entities => this.merge(entities, this.idField),
|
|
49
|
+
confirm,
|
|
50
|
+
notify,
|
|
51
|
+
slience
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async upsert(
|
|
57
|
+
row: Partial<R>,
|
|
58
|
+
conflictFields: strKeyOf<R>[],
|
|
59
|
+
{ confirm, notify, slience }: muteOptions = {}
|
|
60
|
+
) {
|
|
61
|
+
let conflict: any = {}
|
|
62
|
+
for (let f of conflictFields) {
|
|
63
|
+
conflict[f] = row[f]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let { data, error } = await supabase.from<R>(this.tableName).select("*").match(conflict)
|
|
67
|
+
handleError(data, error)
|
|
68
|
+
|
|
69
|
+
if (data.length > 1)
|
|
70
|
+
throw 'Fail to upsert! More than 1 rows matching the conflictFields are found!'
|
|
71
|
+
|
|
72
|
+
if (data.length === 1) {
|
|
73
|
+
let id = data[0][this.idField]
|
|
74
|
+
await this.update(id, row, { confirm, notify, slience })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (data.length === 0) {
|
|
78
|
+
await this.insert(row, { confirm, notify, slience })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async delete(
|
|
85
|
+
id: R[this['idField']],
|
|
86
|
+
{ confirm, notify, slience }: muteOptions = {}
|
|
87
|
+
) {
|
|
88
|
+
await this.API({
|
|
89
|
+
type: 'delete',
|
|
90
|
+
where: this.idWhere(id),
|
|
91
|
+
action: entities => this.discard(this.idWhere(id) as Criteria<T>),
|
|
92
|
+
confirm,
|
|
93
|
+
notify,
|
|
94
|
+
slience
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
type Constructor = new (...args: any[]) => {}
|
|
107
|
+
|
|
108
|
+
export function Empower<RCls extends Constructor, R = InstanceType<RCls>>(BaseCls: RCls) {
|
|
109
|
+
|
|
110
|
+
return class extends BaseCls {
|
|
111
|
+
|
|
112
|
+
public hostTable: Table<any, any> | undefined = undefined;
|
|
113
|
+
|
|
114
|
+
public async update(
|
|
115
|
+
this: this & R,
|
|
116
|
+
row: Partial<R>,
|
|
117
|
+
{ confirm, notify, slience }: muteOptions = {}
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
|
|
120
|
+
if (this.hostTable === undefined) {
|
|
121
|
+
console.error('You must define hostTable before using .update()!')
|
|
122
|
+
console.error(this)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let idField = this.hostTable.idField as keyof R
|
|
127
|
+
let id = this[idField]
|
|
128
|
+
await this.hostTable.update(id, row, { confirm, notify, slience })
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async delete(
|
|
133
|
+
this: this & R,
|
|
134
|
+
{ confirm, notify, slience }: muteOptions = {}
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
|
|
137
|
+
if (this.hostTable === undefined) {
|
|
138
|
+
console.error('You must define hostTable before using .delete()!')
|
|
139
|
+
console.error(this)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let idField = this.hostTable.idField as keyof R
|
|
144
|
+
let id = this[idField]
|
|
145
|
+
await this.hostTable.delete(id, { confirm, notify, slience })
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { BasicTable, Where } from './basic'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
type selectOptions<R> = {
|
|
5
|
+
where?: Where<R>
|
|
6
|
+
// orderBy?: OrderBy<R>
|
|
7
|
+
// limit?: number
|
|
8
|
+
confirm?: [string, string?]
|
|
9
|
+
notify?: string
|
|
10
|
+
slience?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
function chunk<T>(arr: T[], size: number): T[][] {
|
|
18
|
+
if (size <= 0) return []
|
|
19
|
+
let newArr = []
|
|
20
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
21
|
+
newArr.push(arr.slice(i, i + size))
|
|
22
|
+
}
|
|
23
|
+
return newArr
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A subclass of array designed as an ORM.
|
|
28
|
+
* @template T - type of element class
|
|
29
|
+
* @template R - type of database row class
|
|
30
|
+
*/
|
|
31
|
+
export abstract class TableView<T extends R, R extends object> extends BasicTable<T, R>{
|
|
32
|
+
|
|
33
|
+
async select({ where, confirm, notify, slience }: selectOptions<R> = {}) {
|
|
34
|
+
await this.API({
|
|
35
|
+
type: 'select',
|
|
36
|
+
where,
|
|
37
|
+
// orderBy,
|
|
38
|
+
// limit,
|
|
39
|
+
action: entities => this.set(entities),
|
|
40
|
+
// order: true,
|
|
41
|
+
confirm,
|
|
42
|
+
notify,
|
|
43
|
+
slience
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async patch({ where, confirm, notify, slience }: selectOptions<R> = {}) {
|
|
48
|
+
await this.API({
|
|
49
|
+
type: 'patch',
|
|
50
|
+
where,
|
|
51
|
+
// orderBy,
|
|
52
|
+
// limit,
|
|
53
|
+
action: entities => this.absorb(entities, this.idField),
|
|
54
|
+
confirm,
|
|
55
|
+
notify,
|
|
56
|
+
slience
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async patchIDs(
|
|
62
|
+
ids: R[this['idField']][],
|
|
63
|
+
{ where, confirm, notify, slience }: selectOptions<R> = {},
|
|
64
|
+
chunkSize = 100,
|
|
65
|
+
) {
|
|
66
|
+
for (let ck of chunk(ids, chunkSize)) {
|
|
67
|
+
await this.patch({
|
|
68
|
+
where: { ...this.idsWhere(ck), ...where },
|
|
69
|
+
confirm,
|
|
70
|
+
notify,
|
|
71
|
+
slience
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
if (this.ordering.length === 0)
|
|
75
|
+
this.orderById(ids)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async selectIDs(
|
|
80
|
+
ids: R[this['idField']][],
|
|
81
|
+
{ where, confirm, notify, slience }: selectOptions<R> = {},
|
|
82
|
+
chunkSize = 100,
|
|
83
|
+
) {
|
|
84
|
+
this.clear()
|
|
85
|
+
this.patchIDs(
|
|
86
|
+
ids,
|
|
87
|
+
{ where, confirm, notify, slience },
|
|
88
|
+
chunkSize
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
package/src/utils/supabase.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createClient, PostgrestError } from '@supabase/supabase-js'
|
|
1
|
+
import { createClient, PostgrestError } from '@supabase/supabase-js'
|
|
2
2
|
import { qDialog } from './dialog'
|
|
3
3
|
|
|
4
4
|
declare module '@supabase/supabase-js' {
|
|
@@ -19,7 +19,7 @@ if (!process.env.SUPABASE_URL)
|
|
|
19
19
|
if (!process.env.SUPABASE_KEY)
|
|
20
20
|
throw 'Missing SUPABASE_KEY in process.env!'
|
|
21
21
|
|
|
22
|
-
export const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
|
22
|
+
export const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
supabase.signIn = async () => {
|
|
@@ -28,12 +28,12 @@ supabase.signIn = async () => {
|
|
|
28
28
|
provider: 'google'
|
|
29
29
|
}, {
|
|
30
30
|
redirectTo: window.location.href
|
|
31
|
-
})
|
|
32
|
-
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
33
|
|
|
34
34
|
supabase.signOut = async () => {
|
|
35
35
|
await supabase.auth.signOut()
|
|
36
|
-
}
|
|
36
|
+
}
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
supabase.accessTokenLife = () => {
|
|
@@ -57,7 +57,7 @@ supabase.checkSession = async () => {
|
|
|
57
57
|
const { error } = await supabase.auth.refreshSession()
|
|
58
58
|
if (error) {
|
|
59
59
|
print('Refresh failed. Force Signin...')
|
|
60
|
-
supabase.signIn()
|
|
60
|
+
supabase.signIn()
|
|
61
61
|
} else {
|
|
62
62
|
print('Refresh succeed!')
|
|
63
63
|
}
|
|
@@ -69,7 +69,7 @@ supabase.checkSession = async () => {
|
|
|
69
69
|
const { error } = await supabase.auth.refreshSession()
|
|
70
70
|
if (error) {
|
|
71
71
|
print('Refresh failed. Force Signin...')
|
|
72
|
-
supabase.signIn()
|
|
72
|
+
supabase.signIn()
|
|
73
73
|
} else {
|
|
74
74
|
print('Refresh succeed!')
|
|
75
75
|
}
|
|
@@ -80,8 +80,8 @@ supabase.checkSession = async () => {
|
|
|
80
80
|
setInterval(() => supabase.checkSession(), 1000)
|
|
81
81
|
|
|
82
82
|
supabase.auth.onAuthStateChange((event, session) => {
|
|
83
|
-
console.log('[SUPABASE AUTH UPDATE]', event, session?.user?.email)
|
|
84
|
-
})
|
|
83
|
+
console.log('[SUPABASE AUTH UPDATE]', event, session?.user?.email)
|
|
84
|
+
})
|
|
85
85
|
|
|
86
86
|
supabase.email = (): string => {
|
|
87
87
|
return supabase.auth.user()?.email ?? 'unauthenticated'
|
|
@@ -91,14 +91,14 @@ supabase.email = (): string => {
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
supabase.signedIn = false
|
|
94
|
+
supabase.signedIn = false
|
|
95
95
|
|
|
96
96
|
supabase.auth.onAuthStateChange((event, session) => {
|
|
97
97
|
if (event === 'SIGNED_IN') supabase.signedIn = true
|
|
98
|
-
})
|
|
98
|
+
})
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
async function waitFor(predicate: () => boolean, waitingMsg?: string, doneMsg?: string
|
|
101
|
+
async function waitFor(predicate: () => boolean, waitingMsg?: string, doneMsg?: string): Promise<unknown> {
|
|
102
102
|
return new Promise(resolve => {
|
|
103
103
|
function checker() {
|
|
104
104
|
if (predicate()) {
|
|
@@ -106,7 +106,7 @@ async function waitFor(predicate: () => boolean, waitingMsg?: string, doneMsg?:
|
|
|
106
106
|
resolve(true)
|
|
107
107
|
} else {
|
|
108
108
|
if (waitingMsg) console.log(waitingMsg)
|
|
109
|
-
setTimeout(() => checker(), 50)
|
|
109
|
+
setTimeout(() => checker(), 50)
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
checker()
|
|
@@ -134,3 +134,18 @@ export function handleError<T>(data: T[] | null, error: PostgrestError | null):
|
|
|
134
134
|
throw 'supabase return data is null!'
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
const _filterBuilder = supabase.from('DEMO').select()
|
|
141
|
+
export type PostgrestFilterBuilder = typeof _filterBuilder
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
export async function callRPC<T>(fn: string, params?: object | undefined) {
|
|
146
|
+
let { data, error } = await supabase.rpc<T>(fn, params)
|
|
147
|
+
handleError(data, error)
|
|
148
|
+
return data
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
package/src/utils/table.ts
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
import { Notify } from 'quasar'
|
|
2
|
-
import { supabase, handleError } from './supabase'
|
|
3
|
-
import { qDialog } from './dialog'
|
|
4
|
-
import { LoadingBar } from 'quasar'
|
|
5
|
-
import { Ordering, Criteria, strKeyOf, Sheet } from 'sapphire-js'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type Where<T> = { [K in keyof T]?: T[K] | T[K][] | { like: string } } | ((_: FilterBuilder) => any)
|
|
9
|
-
|
|
10
|
-
type OrderBy<T> = strKeyOf<T> | { [K in keyof T]?: true | false }
|
|
11
|
-
|
|
12
|
-
type FilterBuilder = { eq: any, in: any, like: any, or: any, order: any }
|
|
13
|
-
|
|
14
|
-
type muteOptions = {
|
|
15
|
-
confirm?: string | [string, string]
|
|
16
|
-
notify?: string
|
|
17
|
-
slience?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type selectOptions<R> = {
|
|
21
|
-
where?: Where<R>
|
|
22
|
-
orderBy?: OrderBy<R>
|
|
23
|
-
limit?: number
|
|
24
|
-
confirm?: string | [string, string]
|
|
25
|
-
notify?: string
|
|
26
|
-
slience?: boolean
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
enum ActionType {
|
|
31
|
-
select = 'select',
|
|
32
|
-
insert = 'insert',
|
|
33
|
-
update = 'update',
|
|
34
|
-
delete = 'delete',
|
|
35
|
-
patch = 'patch',
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
function getKeys<T>(obj: T): (string & keyof T)[] {
|
|
41
|
-
return Object.keys(obj) as (string & keyof T)[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* A subclass of array designed as an ORM.
|
|
48
|
-
* @template T - type of element class
|
|
49
|
-
* @template R - type of database row class
|
|
50
|
-
*/
|
|
51
|
-
export abstract class Table<T extends R, R extends object> extends Sheet<T>{
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The name of the database table.
|
|
55
|
-
*/
|
|
56
|
-
public abstract readonly tableName: string
|
|
57
|
-
/**
|
|
58
|
-
* The name of the ID field.
|
|
59
|
-
*/
|
|
60
|
-
public abstract readonly idField: strKeyOf<R>
|
|
61
|
-
/**
|
|
62
|
-
* The class of the elements.
|
|
63
|
-
*/
|
|
64
|
-
public abstract readonly entityClass: new (_: Partial<R>) => T
|
|
65
|
-
/**
|
|
66
|
-
* The default ordering instruction object.
|
|
67
|
-
*/
|
|
68
|
-
public readonly ordering?: Readonly<Ordering<T>> = undefined
|
|
69
|
-
/**
|
|
70
|
-
* The default fields to select
|
|
71
|
-
*/
|
|
72
|
-
public readonly selectFields?: strKeyOf<R>[] = undefined
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
private convert(data: R[]): T[] {
|
|
77
|
-
return data.map($ => new this.entityClass($))
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private log(msg: string, data: T[]): void {
|
|
81
|
-
if (process.env.DEBUGGING)
|
|
82
|
-
console.log(`[${this.tableName}] ${msg}`, data)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private async confirm(confirm: string | [string, string]): Promise<void> {
|
|
86
|
-
if (typeof confirm === 'string') {
|
|
87
|
-
await qDialog.confirm(confirm)
|
|
88
|
-
} else {
|
|
89
|
-
let [title, message] = confirm
|
|
90
|
-
await qDialog.confirm(title, message)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private notify(msg: string): void {
|
|
95
|
-
Notify.create({
|
|
96
|
-
message: msg,
|
|
97
|
-
progress: true,
|
|
98
|
-
type: 'positive'
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private loadingBarOff(): void {
|
|
103
|
-
LoadingBar.setDefaults({ size: "0px" })
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private loadingBarOn(): void {
|
|
107
|
-
setTimeout(() => {
|
|
108
|
-
LoadingBar.setDefaults({ size: "5px" })
|
|
109
|
-
}, 3000);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private defaultOrder(): void {
|
|
113
|
-
if (this.ordering !== undefined)
|
|
114
|
-
this.order(...this.ordering)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private idWhere(id: R[this['idField']]): Partial<R> {
|
|
118
|
-
return { [this.idField]: id } as Partial<R>
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private APIAction(type: ActionType, content?: Partial<R> | Partial<R>[]) {
|
|
122
|
-
let q = supabase.from<R>(this.tableName)
|
|
123
|
-
if (type === ActionType.select) {
|
|
124
|
-
let fields = this.selectFields === undefined ? "*" : this.selectFields.join(",")
|
|
125
|
-
return q.select(fields)
|
|
126
|
-
}
|
|
127
|
-
if (type === ActionType.patch) {
|
|
128
|
-
let fields = this.selectFields === undefined ? "*" : this.selectFields.join(",")
|
|
129
|
-
return q.select(fields)
|
|
130
|
-
}
|
|
131
|
-
if (type === ActionType.delete)
|
|
132
|
-
return q.delete()
|
|
133
|
-
if (content === undefined)
|
|
134
|
-
throw 'API Action require content!'
|
|
135
|
-
if (type === ActionType.insert)
|
|
136
|
-
return q.insert(content)
|
|
137
|
-
if (Array.isArray(content))
|
|
138
|
-
throw 'API Action require single object content!'
|
|
139
|
-
if (type === ActionType.update)
|
|
140
|
-
return q.update(content)
|
|
141
|
-
throw 'never'
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private async API({
|
|
145
|
-
type,
|
|
146
|
-
content,
|
|
147
|
-
where,
|
|
148
|
-
orderBy,
|
|
149
|
-
limit,
|
|
150
|
-
action,
|
|
151
|
-
order,
|
|
152
|
-
confirm,
|
|
153
|
-
notify,
|
|
154
|
-
slience
|
|
155
|
-
}: {
|
|
156
|
-
type: ActionType
|
|
157
|
-
content?: Partial<R> | Partial<R>[]
|
|
158
|
-
where?: Where<R>
|
|
159
|
-
orderBy?: OrderBy<R>
|
|
160
|
-
limit?: number,
|
|
161
|
-
action?: (entities: T[]) => void
|
|
162
|
-
order?: boolean
|
|
163
|
-
confirm?: string | [string, string]
|
|
164
|
-
notify?: string
|
|
165
|
-
slience?: boolean
|
|
166
|
-
}) {
|
|
167
|
-
if (slience) this.loadingBarOff()
|
|
168
|
-
if (confirm) await this.confirm(confirm)
|
|
169
|
-
|
|
170
|
-
let q = this.APIAction(type, content)
|
|
171
|
-
if (where !== undefined)
|
|
172
|
-
q = this.applyWhere(q, where)
|
|
173
|
-
if (orderBy !== undefined)
|
|
174
|
-
q = this.applyOrderBy(q, orderBy)
|
|
175
|
-
if (limit !== undefined)
|
|
176
|
-
q = q.limit(limit)
|
|
177
|
-
let { data, error } = await q
|
|
178
|
-
handleError(data, error)
|
|
179
|
-
|
|
180
|
-
let entities = this.convert(data)
|
|
181
|
-
if (action !== undefined) action(entities)
|
|
182
|
-
if (order === true) this.defaultOrder()
|
|
183
|
-
|
|
184
|
-
this.log(type, entities)
|
|
185
|
-
if (notify) this.notify(notify)
|
|
186
|
-
if (slience) this.loadingBarOn()
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// api
|
|
190
|
-
|
|
191
|
-
async select({ where, orderBy, limit, confirm, notify, slience }: selectOptions<R> = {}) {
|
|
192
|
-
await this.API({
|
|
193
|
-
type: ActionType.select,
|
|
194
|
-
where,
|
|
195
|
-
orderBy,
|
|
196
|
-
limit,
|
|
197
|
-
action: entities => this.set(entities),
|
|
198
|
-
order: true,
|
|
199
|
-
confirm,
|
|
200
|
-
notify,
|
|
201
|
-
slience
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
async patch({ where, orderBy, limit, confirm, notify, slience }: selectOptions<R> = {}) {
|
|
207
|
-
await this.API({
|
|
208
|
-
type: ActionType.patch,
|
|
209
|
-
where,
|
|
210
|
-
orderBy,
|
|
211
|
-
limit,
|
|
212
|
-
action: entities => this.absorb(entities, this.idField),
|
|
213
|
-
order: true,
|
|
214
|
-
confirm,
|
|
215
|
-
notify,
|
|
216
|
-
slience
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
async insert(
|
|
222
|
-
rows: Partial<R> | Partial<R>[],
|
|
223
|
-
{ confirm, notify, slience }: muteOptions = {}
|
|
224
|
-
) {
|
|
225
|
-
await this.API({
|
|
226
|
-
type: ActionType.insert,
|
|
227
|
-
content: Array.isArray(rows) ? rows : [rows],
|
|
228
|
-
action: entities => this.push(...entities),
|
|
229
|
-
order: true,
|
|
230
|
-
confirm,
|
|
231
|
-
notify,
|
|
232
|
-
slience
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
async update(
|
|
238
|
-
id: R[this['idField']],
|
|
239
|
-
row: Partial<R>,
|
|
240
|
-
{ confirm, notify, slience }: muteOptions = {}
|
|
241
|
-
) {
|
|
242
|
-
await this.API({
|
|
243
|
-
type: ActionType.update,
|
|
244
|
-
content: row,
|
|
245
|
-
where: this.idWhere(id),
|
|
246
|
-
action: entities => this.merge(entities, this.idField),
|
|
247
|
-
confirm,
|
|
248
|
-
notify,
|
|
249
|
-
slience
|
|
250
|
-
})
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
async upsert(
|
|
255
|
-
row: Partial<R>,
|
|
256
|
-
conflictFields: strKeyOf<R>[],
|
|
257
|
-
{ confirm, notify, slience }: muteOptions = {}
|
|
258
|
-
) {
|
|
259
|
-
let conflict: any = {}
|
|
260
|
-
for (let f of conflictFields) {
|
|
261
|
-
conflict[f] = row[f]
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
let { data, error } = await supabase.from<R>(this.tableName).select("*").match(conflict)
|
|
265
|
-
handleError(data, error)
|
|
266
|
-
|
|
267
|
-
if (data.length > 1)
|
|
268
|
-
throw 'Fail to upsert! More than 1 rows matching the conflictFields are found!'
|
|
269
|
-
|
|
270
|
-
if (data.length === 1) {
|
|
271
|
-
let id = data[0][this.idField]
|
|
272
|
-
await this.update(id, row, { confirm, notify, slience })
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (data.length === 0) {
|
|
276
|
-
await this.insert(row, { confirm, notify, slience })
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
async delete(
|
|
283
|
-
id: R[this['idField']],
|
|
284
|
-
{ confirm, notify, slience }: muteOptions = {}
|
|
285
|
-
) {
|
|
286
|
-
await this.API({
|
|
287
|
-
type: ActionType.delete,
|
|
288
|
-
where: this.idWhere(id),
|
|
289
|
-
action: entities => this.discard(this.idWhere(id) as Criteria<T>),
|
|
290
|
-
confirm,
|
|
291
|
-
notify,
|
|
292
|
-
slience
|
|
293
|
-
})
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
private applyWhere<T extends FilterBuilder>(q: T, where: Where<R>): T {
|
|
298
|
-
if (typeof where === 'function') {
|
|
299
|
-
q = where(q)
|
|
300
|
-
} else {
|
|
301
|
-
for (let key in where) {
|
|
302
|
-
let val = where[key]
|
|
303
|
-
if (Array.isArray(val)) {
|
|
304
|
-
// treat as in
|
|
305
|
-
q = q.in(key, val)
|
|
306
|
-
} else if (typeof val === 'object') {
|
|
307
|
-
if ('like' in val) {
|
|
308
|
-
// treat as like
|
|
309
|
-
let { like } = val as { like: string }
|
|
310
|
-
q = q.like(key, like)
|
|
311
|
-
}
|
|
312
|
-
} else {
|
|
313
|
-
// treat as eq
|
|
314
|
-
q = q.eq(key, val)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return q
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private applyOrderBy<T extends FilterBuilder>(q: T, orderBy: OrderBy<R>): T {
|
|
323
|
-
if (typeof orderBy === "string") {
|
|
324
|
-
q = q.order(orderBy)
|
|
325
|
-
} else {
|
|
326
|
-
let field = getKeys(orderBy)[0]
|
|
327
|
-
let ascending = orderBy[field]
|
|
328
|
-
q = q.order(field, { ascending })
|
|
329
|
-
}
|
|
330
|
-
return q
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Get an item by `criteria`. If not found, return a new object.
|
|
335
|
-
*/
|
|
336
|
-
got(criteria: Criteria<T>): T {
|
|
337
|
-
return this.get(criteria) ?? new this.entityClass({})
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
type Constructor = new (...args: any[]) => {};
|
|
347
|
-
|
|
348
|
-
export function Empower<RCls extends Constructor, R = InstanceType<RCls>>(BaseCls: RCls) {
|
|
349
|
-
|
|
350
|
-
return class extends BaseCls {
|
|
351
|
-
|
|
352
|
-
public hostTable: Table<any, any> | undefined = undefined;
|
|
353
|
-
|
|
354
|
-
public async update(
|
|
355
|
-
this: this & R,
|
|
356
|
-
row: Partial<R>,
|
|
357
|
-
{ confirm, notify, slience }: muteOptions = {}
|
|
358
|
-
): Promise<void> {
|
|
359
|
-
|
|
360
|
-
if (this.hostTable === undefined) {
|
|
361
|
-
console.error('You must define hostTable before using .updateThis()!')
|
|
362
|
-
console.error(this)
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
let idField = this.hostTable.idField as keyof R
|
|
367
|
-
let id = this[idField]
|
|
368
|
-
await this.hostTable.update(id, row, { confirm, notify, slience })
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
public async delete(
|
|
373
|
-
this: this & R,
|
|
374
|
-
{ confirm, notify, slience }: muteOptions = {}
|
|
375
|
-
): Promise<void> {
|
|
376
|
-
|
|
377
|
-
if (this.hostTable === undefined) {
|
|
378
|
-
console.error('You must define hostTable before using .deleteThis()!')
|
|
379
|
-
console.error(this)
|
|
380
|
-
return
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
let idField = this.hostTable.idField as keyof R
|
|
384
|
-
let id = this[idField]
|
|
385
|
-
await this.hostTable.delete(id, { confirm, notify, slience })
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|