@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d-mok/quasar-app-extension-quasar-axe",
3
- "version": "0.0.94",
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.1",
24
- "@types/papaparse": "^5.2.6",
25
- "@types/webpack-env": "^1.16.2",
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.61"
27
+ "sapphire-js": "^1.0.63"
28
28
  },
29
29
  "devDependencies": {
30
- "@quasar/app": "^3.2.5",
31
- "@quasar/extras": "^1.12.2",
32
- "@types/node": "^17.0.4",
33
- "core-js": "^3.20.0",
34
- "quasar": "^2.3.4",
35
- "typescript": "^4.5.4"
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
  }
@@ -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', true, /[\w-]+\.vue$/)
14
+ const pages = require.context('src/pages', false, /[\w-]+\.vue$/)
15
15
  router.addRoute({
16
16
  path: '/',
17
17
  //@ts-ignore
@@ -12,7 +12,9 @@
12
12
 
13
13
  <q-toolbar-title class="text-weight-bolder text-h5">{{ title }}</q-toolbar-title>
14
14
 
15
- <div class="text-h6">/{{ appendix }}</div>
15
+ <slot name="appendix">
16
+ <div class="text-h6">/{{ appendix }}</div>
17
+ </slot>
16
18
  </q-toolbar>
17
19
  </q-header>
18
20
 
@@ -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
+
@@ -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
+
@@ -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
-