@01-edu/shared 2.0.4 → 2.0.5

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/hasura-core.js ADDED
@@ -0,0 +1,217 @@
1
+ export class HasuraError extends Error {
2
+ constructor({ extensions, message, ...props }, cause) {
3
+ super(message, cause)
4
+ Object.assign(this, props)
5
+ Object.assign(this, extensions)
6
+ Error.captureStackTrace?.(this, HasuraError)
7
+ }
8
+ }
9
+
10
+ export const initClient = ({ debug, address, log, ...params }) => {
11
+ log || (log = debug ? console.debug : () => {})
12
+ const handlers = new Map()
13
+ const subscribers = new Map()
14
+
15
+ const getId = () => {
16
+ const id = Math.random().toString(36).slice(2)
17
+ return handlers.has(id) ? getId() : id
18
+ }
19
+
20
+ const rejectAllPending = err => {
21
+ subscribers.clear() // TODO: store subscribers query and re-trigger them
22
+ for (const [id, { reject, noCleanup }] of handlers) {
23
+ noCleanup || activeQueries.delete(id)
24
+ handlers.delete(id)
25
+ reject(err)
26
+ }
27
+ return err
28
+ }
29
+
30
+ const end = (handler, props = {}) => {
31
+ props.duration = Date.now() - handler.start
32
+ props.size = handler.size
33
+ props.name = handler.query
34
+ props.id = handler.id
35
+ log('query', props)
36
+ handlers.delete(handler.id)
37
+ handler.noCleanup || activeQueries.delete(handler.id)
38
+ }
39
+
40
+ const messageFail = (handler, payload, id) => {
41
+ if (!handler) return log('missing-handler', { id, type: 'error' })
42
+
43
+ end(handler, { payload, type: 'error' })
44
+ handlers.delete(id)
45
+ return handler.reject(
46
+ new HasuraError(payload.errors[0], debug && { cause: handler.cause }),
47
+ )
48
+ }
49
+
50
+ const handleMessage = (data, resolve, reject) => {
51
+ if (data === '{"type":"ka"}') return // ignore keep alive
52
+
53
+ const { type, payload, id } = JSON.parse(data)
54
+ const handler = handlers.get(id)
55
+ handler && (handler.size += data.length)
56
+
57
+ log('raw', data)
58
+
59
+ switch (type) {
60
+ case 'connection_ack': {
61
+ return resolve(payload)
62
+ }
63
+
64
+ case 'connection_error': {
65
+ const err = rejectAllPending(new HasuraError({ errors: [payload] }))
66
+ return reject(err)
67
+ }
68
+
69
+ case 'data': {
70
+ if (payload.errors) return messageFail(handler, payload, id)
71
+
72
+ const sub = subscribers.get(id)
73
+ if (!sub) {
74
+ return handler
75
+ ? (handler.payload = payload)
76
+ : log('missing-handler', { id, type: 'error' })
77
+ }
78
+
79
+ sub(payload.data)
80
+ if (handler) {
81
+ end(handler, { type, payload })
82
+ handler.resolve()
83
+ }
84
+
85
+ return
86
+ }
87
+
88
+ case 'error': {
89
+ return messageFail(handler, payload, id)
90
+ }
91
+
92
+ case 'complete': {
93
+ if (!handler) return
94
+ end(handler, { type, payload })
95
+ return handler.resolve(handler.payload?.data)
96
+ }
97
+ }
98
+ }
99
+
100
+ const handleFail = (event, type) =>
101
+ rejectAllPending(
102
+ new HasuraError({ message: `WebSocket connection ${type}`, event }),
103
+ )
104
+
105
+ let ws = new WebSocket(address, 'graphql-ws')
106
+ let activeQueries = new Map()
107
+ const exec = async (id, payload, name, noCleanup) => {
108
+ await connection
109
+ const handler = {
110
+ id,
111
+ size: 0,
112
+ query: name,
113
+ start: Date.now(),
114
+ noCleanup,
115
+ }
116
+ const result = new Promise((resolve, reject) => {
117
+ handler.resolve = resolve
118
+ handler.reject = reject
119
+ })
120
+ debug && (handler.cause = Error('hasuraClient.exec'))
121
+ handlers.set(id, handler)
122
+ activeQueries.set(id, { payload, name })
123
+ log('start', { id, payload })
124
+ ws.send(`{"type":"start","id":"${id}","payload":${payload}}`)
125
+ return result
126
+ }
127
+ const runFromString = (payload, name) => exec(getId(), payload, name)
128
+
129
+ const subscribeFromString = (sub, payload, name) => {
130
+ const id = getId()
131
+ subscribers.set(id, sub)
132
+
133
+ return {
134
+ execution: exec(id, payload, name, true),
135
+ unsubscribe: () => {
136
+ subscribers.delete(id)
137
+ activeQueries.delete(id)
138
+ log('stop', { id })
139
+ ws.send(`{"type":"stop","id":"${id}"}`)
140
+ },
141
+ }
142
+ }
143
+
144
+ let connected = false
145
+ const getConnection = async () => {
146
+ connected = false
147
+ const { promise, resolve, reject } = Promise.withResolvers()
148
+ const onError = () => reject(handleFail(undefined, 'failed'))
149
+ const onClose = event =>
150
+ reject(handleFail({ code: event.code, reason: event.reason }, 'close'))
151
+ ws.addEventListener('error', onError, { once: true })
152
+ ws.addEventListener('close', onClose, { once: true })
153
+ ws.addEventListener('message', event =>
154
+ handleMessage(event.data, resolve, reject),
155
+ )
156
+ try {
157
+ await promise
158
+ return (connected = true)
159
+ } finally {
160
+ ws.removeEventListener('error', onError)
161
+ ws.removeEventListener('close', onClose)
162
+ }
163
+ }
164
+
165
+ let connection = getConnection()
166
+
167
+ const connect = async ({ adminSecret, token, role, headers }) => {
168
+ const previousActiveQueries = activeQueries
169
+ const reload = connected
170
+ if (reload) {
171
+ ws.close()
172
+ ws = new WebSocket(address, 'graphql-ws')
173
+ connection = getConnection()
174
+ activeQueries = new Map()
175
+ }
176
+
177
+ if (!ws.readyState) {
178
+ await new Promise(s => ws.addEventListener('open', s, { once: true }))
179
+ }
180
+
181
+ const payload = {
182
+ headers: adminSecret
183
+ ? { 'x-hasura-admin-secret': adminSecret, ...headers }
184
+ : { Authorization: `Bearer ${token}`, ...headers },
185
+ }
186
+
187
+ role && (payload.headers['x-hasura-role'] = role)
188
+
189
+ ws.send(JSON.stringify({ type: 'connection_init', payload }))
190
+
191
+ reload &&
192
+ connection.then(() => {
193
+ // re exec all previous active queries
194
+ for (const [id, { payload, name }] of previousActiveQueries) {
195
+ exec(id, payload, name)
196
+ }
197
+ })
198
+
199
+ return connection
200
+ }
201
+
202
+ if (params.adminSecret || params.token) {
203
+ connect(params)
204
+ }
205
+
206
+ return {
207
+ ws,
208
+ connect,
209
+ connection,
210
+ runFromString,
211
+ subscribeFromString,
212
+ run: (query, variables) =>
213
+ runFromString(JSON.stringify({ query, variables })),
214
+ subscribe: (sub, query, variables) =>
215
+ subscribeFromString(sub, JSON.stringify({ query, variables })),
216
+ }
217
+ }
@@ -0,0 +1,138 @@
1
+ export const buildModel =
2
+ prepare =>
3
+ (name, key = 'id', type = 'Int') => {
4
+ const list = `${key}_list`
5
+ const insertQuery = prepare(`
6
+ mutation insert_${name} ($objects: [${name}_insert_input!]!){
7
+ insert_${name} (objects: $objects) { returning { ${key} } }
8
+ }`)
9
+
10
+ const updateQuery = prepare(`
11
+ mutation update_${name} ($${key}: ${type}!, $changes: ${name}_set_input!) {
12
+ update_${name} (where: {${key}: {_eq: $${key}}}, _set: $changes) { affected_rows }
13
+ }`)
14
+
15
+ const updateQueryAll = prepare(`
16
+ mutation update_${name} ($${list}: [${type}!], $changes: ${name}_set_input!) {
17
+ update_${name} (where: {${key}: {_in: $${list}}}, _set: $changes) { affected_rows }
18
+ }`)
19
+
20
+ const deleteQuery = prepare(`
21
+ mutation delete_${name} ($${key}: ${type}!) {
22
+ delete_${name} (where: {${key}: {_eq: $${key}}}) { affected_rows }
23
+ }`)
24
+
25
+ const deleteQueryAll = prepare(`
26
+ mutation delete_${name} ($${list}: [${type}!]) {
27
+ delete_${name} (where: {id: {_in: $${list}} }) { affected_rows }
28
+ }`)
29
+
30
+ const getCountQuery = prepare(`
31
+ query ${name}_count {
32
+ ${name}_aggregate { aggregate { count } }
33
+ }`)
34
+
35
+ const getKey = _ => _[key]
36
+ const updateOne = ({ [key]: _, ...changes }) =>
37
+ updateQuery({ [key]: _, changes })
38
+
39
+ const mutations = {
40
+ key,
41
+ list,
42
+ insertQuery,
43
+ deleteQuery,
44
+ updateQuery,
45
+ updateQueryAll,
46
+ deleteQueryAll,
47
+ remove: _ =>
48
+ Array.isArray(_)
49
+ ? deleteQueryAll({ [list]: _ })
50
+ : deleteQuery({ [key]: _ }),
51
+ update: (changes, _) => {
52
+ if (!_) return updateOne(changes)
53
+ return Array.isArray(_)
54
+ ? updateQueryAll({ changes, [list]: _ })
55
+ : updateQuery({ changes, [key]: _ })
56
+ },
57
+ add: async _ => {
58
+ const isArray = Array.isArray(_)
59
+ const result = await insertQuery.all({ objects: isArray ? _ : [_] })
60
+
61
+ return isArray
62
+ ? result[`insert_${name}`].returning.map(getKey)
63
+ : result[`insert_${name}`].returning[0][key]
64
+ },
65
+ }
66
+
67
+ return fields => {
68
+ const oneById = `($${key}: ${type}!) {
69
+ ${name} (where: {${key}: {_eq: $${key}}} limit: 1) {${key} ${fields}}
70
+ }`
71
+
72
+ const allById = `($${list}: [${type}!]) {
73
+ ${name} (where: {${key}: {_in: $${list}}}) {${key} ${fields}}
74
+ }`
75
+
76
+ const byWhere = `($where: ${name}_bool_exp!) {
77
+ ${name} (where: $where) {${key} ${fields}}
78
+ }`
79
+
80
+ const toPaginate = `(
81
+ $where: ${name}_bool_exp!, $orderBy: ${name}_order_by!, $limit: Int!, $offset: Int!,
82
+ ) {
83
+ ${name} ( order_by: [$orderBy] offset: $offset limit: $limit where: $where ) { ${fields} }
84
+ }`
85
+
86
+ const toPaginateWithCount = `(
87
+ $where: ${name}_bool_exp!, $orderBy: ${name}_order_by!, $limit: Int!, $offset: Int!,
88
+ ) {
89
+ ${name} ( order_by: [$orderBy] offset: $offset limit: $limit where: $where ) { ${fields} }
90
+ ${name}_aggregate (where: $where) { aggregate { count } }
91
+ }`
92
+
93
+ const selectQuery = prepare(`query ${oneById}`)
94
+ const selectQueryAll = prepare(`query ${allById}`)
95
+ const selectQueryWhere = prepare(`query ${byWhere}`)
96
+ const selectQueryPaginated = prepare(
97
+ `query get_${name}_paginate ${toPaginate}`,
98
+ )
99
+ const selectQueryPaginatedWithCount = prepare(
100
+ `query get_${name}_with_count ${toPaginateWithCount}`,
101
+ )
102
+ const subscribeQuery = prepare(`subscription ${oneById}`)
103
+ const subscribeQueryAll = prepare(`subscription ${allById}`)
104
+ const subscribeQueryWhere = prepare(`subscription ${byWhere}`)
105
+
106
+ return {
107
+ ...mutations,
108
+ selectQuery,
109
+ selectQueryAll,
110
+ selectQueryWhere,
111
+ subscribeQuery,
112
+ subscribeQueryAll,
113
+ subscribeQueryWhere,
114
+ get: _ => {
115
+ if (Array.isArray(_)) return selectQueryAll({ [list]: _ })
116
+ return _ && typeof _ === 'object'
117
+ ? selectQueryWhere({ where: _ })
118
+ : selectQuery.one(_ && { [key]: _ })
119
+ },
120
+ subscribe: (sub, _) => {
121
+ if (Array.isArray(_)) return subscribeQueryAll(sub, { [list]: _ })
122
+ return _ && typeof _ === 'object'
123
+ ? subscribeQueryWhere(sub, { where: _ })
124
+ : subscribeQuery.one(sub, _ && { [key]: _ })
125
+ },
126
+ getCount: async elems => (await getCountQuery(elems)).aggregate.count,
127
+ getPaginated: selectQueryPaginated,
128
+ getPaginatedWithCount: async elems => {
129
+ const elemsWithCount = await selectQueryPaginatedWithCount.all(elems)
130
+
131
+ return {
132
+ [name]: elemsWithCount[name],
133
+ count: elemsWithCount[`${name}_aggregate`].aggregate.count,
134
+ }
135
+ },
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,44 @@
1
+ const get = _ => Object.values(_)[0]
2
+ const getAll = _ => _
3
+ const getOne = _ => Object.values(_)[0][0]
4
+ export const prepare = ({ runFromString, subscribeFromString }, query) => {
5
+ if (typeof query !== 'string') {
6
+ throw Error(`Query must be a string but was ${typeof query}`)
7
+ }
8
+
9
+ let [_, type, name] = query.split(/^\s*(\w+)(?:\s+(\w+))?\b/)
10
+ if (!type) {
11
+ type = 'query'
12
+ } else if (!/^(subscription|mutation|query)$/.test(type)) {
13
+ throw Error(`Invalid query, type must be query, mutation or subscription`)
14
+ }
15
+ name || (name = `${type}_${query.split(/{\s*(.+?)\b/)[1]}`)
16
+ const payload = JSON.stringify({ query })
17
+ const noVars = payload
18
+ const base = payload.slice(0, -1)
19
+ const build = vars => {
20
+ if (!vars) return noVars
21
+ if (typeof vars === 'function') {
22
+ throw Error(
23
+ 'variables should not be functions, verify the order of your parameters',
24
+ )
25
+ }
26
+ const stringified = JSON.stringify(vars)
27
+ if (stringified === '{}') return noVars
28
+ return `${base},"variables":${stringified}}`
29
+ }
30
+ const map =
31
+ type === 'subscription'
32
+ ? mapper => (sub, vars) =>
33
+ subscribeFromString(value => sub(mapper(value)), build(vars), name)
34
+ : mapper => async vars => mapper(await runFromString(build(vars), name))
35
+
36
+ const run = map(get)
37
+ run.all = map(getAll)
38
+ run.one = map(getOne)
39
+ run.map = map
40
+ run.query = query
41
+ return run
42
+ }
43
+
44
+ export const initPrepare = client => query => prepare(client, query)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@01-edu/shared",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "description": "",
6
6
  "scripts": {
@@ -13,6 +13,9 @@
13
13
  "./attrs.js",
14
14
  "./attrs-defs.js",
15
15
  "./graph.js",
16
+ "./hasura-core.js",
17
+ "./hasura-model.js",
18
+ "./hasura-prepare.js",
16
19
  "./languages.js",
17
20
  "./definitions-checker.js",
18
21
  "./path.js",
@@ -20,8 +23,12 @@
20
23
  "./score.js",
21
24
  "./skill-definitions.js",
22
25
  "./toolbox.js",
23
- "./object-structure.ts"
26
+ "./object-structure.js",
27
+ "./qa-utils.js"
24
28
  ],
29
+ "devDependencies": {
30
+ "esbuild": "^0.25.12"
31
+ },
25
32
  "license": "Fair",
26
33
  "bin": {
27
34
  "check-defs": "./bin/check-definitions.js"
package/qa-utils.js ADDED
@@ -0,0 +1,13 @@
1
+ export const stringifyJSX = value => {
2
+ if (!value) return ''
3
+ if (typeof value === 'string') return value
4
+ if (Array.isArray(value)) return value.map(stringifyJSX).join('')
5
+ return stringifyJSX(value.props?.children)
6
+ }
7
+
8
+ export const toTestId = value =>
9
+ stringifyJSX(value)
10
+ .toLowerCase()
11
+ .replace(/([^a-z0-9]+)/g, ' ')
12
+ .trim()
13
+ .replaceAll(' ', '-')
@@ -1,60 +0,0 @@
1
- export const onboardingTypes = new Set([
2
- 'onboarding',
3
- 'piscine-registration',
4
- 'interview',
5
- 'games',
6
- 'administration',
7
- 'module-registration',
8
- 'form-step',
9
- 'sign-step',
10
- 'upload-step',
11
- 'contact-validation-step',
12
- 'avatar-step',
13
- ] as const)
14
-
15
- type Extract<T> = [T] extends [Set<infer V>] ? V : never
16
-
17
- export const objectTypes = new Set([
18
- 'module',
19
- 'piscine',
20
- 'exam',
21
- 'raid',
22
- 'quest',
23
- 'exercise',
24
- 'project',
25
- 'signup',
26
- 'campus',
27
- ...onboardingTypes,
28
- ] as const)
29
-
30
- export type ObjectType = Extract<typeof objectTypes>
31
- export const childTypes = {
32
- campus: ['signup', 'onboarding', 'piscine', 'module'],
33
- signup: [
34
- 'form-step',
35
- 'sign-step',
36
- 'upload-step',
37
- 'contact-validation-step',
38
- 'avatar-step',
39
- ],
40
- onboarding: [
41
- 'games',
42
- 'administration',
43
- 'interview',
44
- 'piscine-registration',
45
- 'module-registration',
46
- ],
47
- administration: [
48
- 'form-step',
49
- 'sign-step',
50
- 'upload-step',
51
- 'contact-validation-step',
52
- 'avatar-step',
53
- ],
54
- piscine: ['quest', 'exam', 'raid', 'project'],
55
- exam: ['exercise'],
56
- quest: ['exercise'],
57
- module: ['project', 'piscine', 'exam'],
58
- } as const satisfies Partial<Record<ObjectType, ObjectType[]>>
59
-
60
- export type ChildTypes = typeof childTypes