@cuboapp/api-backend 3.0.16 → 4.0.0
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/dist/constants/index.d.ts +35 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/core/index.d.ts +77 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/crdt/index.d.ts +45 -0
- package/dist/crdt/index.d.ts.map +1 -0
- package/dist/dialects/index.d.ts +21 -0
- package/dist/dialects/index.d.ts.map +1 -0
- package/dist/helpers/convert.d.ts +11 -0
- package/dist/helpers/convert.d.ts.map +1 -0
- package/dist/helpers/data.d.ts +5 -0
- package/dist/helpers/data.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +42 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/withes.d.ts +10 -0
- package/dist/helpers/withes.d.ts.map +1 -0
- package/dist/hooks/constants.d.ts +7 -0
- package/dist/hooks/constants.d.ts.map +1 -0
- package/dist/hooks/define.d.ts +63 -0
- package/dist/hooks/define.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/subscribe.d.ts +83 -0
- package/dist/hooks/subscribe.d.ts.map +1 -0
- package/dist/hooks/types.d.ts +60 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/query/compile.d.ts +14 -0
- package/dist/query/compile.d.ts.map +1 -0
- package/dist/query/index.d.ts +3 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/types.d.ts +78 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/types/basic.d.ts +60 -0
- package/dist/types/basic.d.ts.map +1 -0
- package/dist/types/db.d.ts +58 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/index.d.ts +37 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +31 -21
- package/src/constants/index.ts +30 -20
- package/src/core/index.ts +6 -5
- package/src/crdt/index.ts +67 -13
- package/src/helpers/data.ts +14 -2
- package/src/helpers/index.ts +101 -46
- package/src/helpers/withes.ts +1 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/subscribe.ts +160 -0
- package/src/hooks/types.ts +6 -3
- package/src/types/basic.ts +2 -2
- package/src/types/db.ts +1 -1
- package/.prettierrc +0 -8
- package/tsconfig.build.json +0 -4
- package/tsconfig.json +0 -22
- package/tsconfig.tsbuildinfo +0 -1
package/src/helpers/index.ts
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
CUBO_CRUD_MAX_PAGE_LIMIT,
|
|
10
10
|
CUBO_CRUD_QUERY_CONDITION,
|
|
11
11
|
CUBO_CRUD_QUERY_KEY_REGEX,
|
|
12
|
-
CUBO_CRUD_QUERY_SYMBOL_NOT
|
|
12
|
+
CUBO_CRUD_QUERY_SYMBOL_NOT,
|
|
13
|
+
CUBO_CRUD_QUERY_SYMBOL_OR
|
|
13
14
|
} from '../constants'
|
|
14
15
|
import type { CuboBackendApi } from '../core'
|
|
15
16
|
import { CuboDialect, getDialect } from '../dialects'
|
|
@@ -69,37 +70,61 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A = unknown> {
|
|
|
69
70
|
let is_not = false
|
|
70
71
|
let condition = 'eq'
|
|
71
72
|
|
|
73
|
+
// Read the condition off the PREFIX when the value carries one, and leave the rest verbatim.
|
|
74
|
+
//
|
|
75
|
+
// The parse below derives the condition by counting `value.split(':')`, which means a value
|
|
76
|
+
// containing a colon of its own has no way through it: `gte:2026-08-01 00:00:00` is four parts
|
|
77
|
+
// and gets rejected, as does a `btw` whose bounds are timestamps. Timestamps are the obvious
|
|
78
|
+
// case, but any text with a colon in it has the same problem.
|
|
79
|
+
//
|
|
80
|
+
// Only a RECOGNISED `[not:]<condition>:` prefix takes this path, so nothing that works today
|
|
81
|
+
// changes and nothing that is rejected today is quietly accepted: `typo:5` still has no known
|
|
82
|
+
// condition, still falls through, and still fails loudly. A `btw` range whose bounds are
|
|
83
|
+
// themselves colon-bearing separates them with `..` rather than `:`.
|
|
84
|
+
// Longest first, so `gte` is not shadowed by `gt` and `ilike` not by `in`.
|
|
85
|
+
const known = Object.values(CUBO_CRUD_QUERY_CONDITION)
|
|
86
|
+
.slice()
|
|
87
|
+
.sort((a, b) => b.length - a.length)
|
|
88
|
+
.join('|')
|
|
89
|
+
const prefixed = typeof value === 'string' ? new RegExp(`^(${CUBO_CRUD_QUERY_SYMBOL_NOT}:)?(${known}):`).exec(value) : null
|
|
90
|
+
|
|
72
91
|
// condition parts
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
if (prefixed) {
|
|
93
|
+
is_not = Boolean(prefixed[1])
|
|
94
|
+
condition = prefixed[2]
|
|
95
|
+
value = (value as string).slice(prefixed[0].length)
|
|
96
|
+
} else {
|
|
97
|
+
switch (parts.length) {
|
|
98
|
+
case 3:
|
|
99
|
+
if (parts[0] === 'btw') {
|
|
100
|
+
condition = parts[0]
|
|
101
|
+
value = `${parts[1]}:${parts[2]}`
|
|
102
|
+
} else if (parts[0] !== CUBO_CRUD_QUERY_SYMBOL_NOT) {
|
|
103
|
+
throw new Error(`"${field.alias}": three-level value allow only "not" condition at first level (${value})`)
|
|
104
|
+
} else if (!Object.values(CUBO_CRUD_QUERY_CONDITION).includes(parts[1])) {
|
|
105
|
+
throw new Error(`"${field.alias}": incorrect condition "${parts[1]}" in second level (${value})`)
|
|
106
|
+
} else {
|
|
107
|
+
is_not = true
|
|
108
|
+
condition = parts[1]
|
|
109
|
+
value = parts[2]
|
|
110
|
+
}
|
|
111
|
+
break
|
|
112
|
+
case 2:
|
|
113
|
+
if (parts[0] === CUBO_CRUD_QUERY_SYMBOL_NOT) {
|
|
114
|
+
is_not = true
|
|
115
|
+
value = parts[1]
|
|
116
|
+
} else if (Object.values(CUBO_CRUD_QUERY_CONDITION).includes(parts[0])) {
|
|
117
|
+
condition = parts[0]
|
|
118
|
+
value = parts[1]
|
|
119
|
+
} else {
|
|
120
|
+
throw new Error(`"${field.alias}": incorrect condition "${parts[0]}" in second level (${value})`)
|
|
121
|
+
}
|
|
122
|
+
break
|
|
123
|
+
case 1:
|
|
124
|
+
break
|
|
125
|
+
default:
|
|
126
|
+
throw new Error(`"${field.alias}": incorrect condition value "${value}"`)
|
|
127
|
+
}
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
switch (field.type) {
|
|
@@ -158,7 +183,7 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A = unknown> {
|
|
|
158
183
|
if ([CUBO_CRUD_QUERY_CONDITION.IN, CUBO_CRUD_QUERY_CONDITION.NIN].includes(condition)) {
|
|
159
184
|
value = value.split(',').map(Number)
|
|
160
185
|
} else if (condition === CUBO_CRUD_QUERY_CONDITION.BTW) {
|
|
161
|
-
const [fromStr, toStr] = value.split(':')
|
|
186
|
+
const [fromStr, toStr] = value.includes('..') ? value.split('..') : value.split(':')
|
|
162
187
|
const from = precision === 0 ? parseInt(fromStr) : parseFloat(fromStr)
|
|
163
188
|
const to = precision === 0 ? parseInt(toStr) : parseFloat(toStr)
|
|
164
189
|
|
|
@@ -284,7 +309,10 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A = unknown> {
|
|
|
284
309
|
replacements[replacement] = value
|
|
285
310
|
break
|
|
286
311
|
case CUBO_CRUD_QUERY_CONDITION.BTW: {
|
|
287
|
-
|
|
312
|
+
// `..` when the bounds carry their own colons (timestamps), `:` otherwise. Splitting on `:`
|
|
313
|
+
// unconditionally would take `00` as the whole upper bound of a `10:00:00` range.
|
|
314
|
+
const raw = String(value)
|
|
315
|
+
const [from, to] = raw.includes('..') ? raw.split('..') : raw.split(':')
|
|
288
316
|
const fromKey = `${replacement}_from`
|
|
289
317
|
const toKey = `${replacement}_to`
|
|
290
318
|
|
|
@@ -464,7 +492,7 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A = unknown> {
|
|
|
464
492
|
return this.api.options.entities.find((e) => e.alias === alias)
|
|
465
493
|
}
|
|
466
494
|
|
|
467
|
-
public async getEntityById(id:
|
|
495
|
+
public async getEntityById(id: string): Promise<CuboEntity | undefined> {
|
|
468
496
|
return this.api.options.entities.find((e) => e.id === id)
|
|
469
497
|
}
|
|
470
498
|
|
|
@@ -488,31 +516,58 @@ export class ApiHelpers<T extends CuboApiEntitiesMap<T>, A = unknown> {
|
|
|
488
516
|
const { sort, limit: _limit, page, with: _withes, ...query } = cloneDeep(req.query || {})
|
|
489
517
|
|
|
490
518
|
// plain conditions (nested are in withes.ts)
|
|
519
|
+
//
|
|
520
|
+
// Counted, because a field can now legitimately be named by two different keys — `?a=1&a|b=2` —
|
|
521
|
+
// and a replacement name derived from the field alone would have the second silently overwrite
|
|
522
|
+
// the first, changing what the first condition filters on rather than failing.
|
|
523
|
+
let conditionIndex = 0
|
|
491
524
|
for (const key in query || {}) {
|
|
492
525
|
const parts = key.replace(CUBO_CRUD_QUERY_KEY_REGEX, '')?.split('.')
|
|
493
526
|
|
|
494
527
|
if (parts.length === 1) {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
528
|
+
// A key may name several fields to be OR-ed: `?a|b=<value>` asks the same condition of each
|
|
529
|
+
// and matches a row where ANY of them holds. One field is just the one-element case, so
|
|
530
|
+
// there is a single path through here rather than two that can drift apart.
|
|
531
|
+
const aliases = parts[0].split(CUBO_CRUD_QUERY_SYMBOL_OR).filter((a) => a.length > 0)
|
|
532
|
+
if (!aliases.length) continue
|
|
533
|
+
conditionIndex += 1
|
|
534
|
+
|
|
535
|
+
const anyOf: string[] = []
|
|
536
|
+
for (const alias of aliases) {
|
|
537
|
+
const conditionField = entity.fields?.find((f) => f.alias === alias)
|
|
538
|
+
const defaultField = CUBO_CRUD_DEFAULT_FIELDS.find((f) => f.alias === alias)
|
|
539
|
+
|
|
540
|
+
if (!conditionField && !defaultField) {
|
|
541
|
+
throw new Error('condition field "' + alias + '" not found in entity "' + entity.alias + '"')
|
|
542
|
+
}
|
|
501
543
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
544
|
+
// Upstream replaced the throw above with `if (conditionField || defaultField)`, skipping an
|
|
545
|
+
// unknown field silently. Not adopted: a filter naming a field that does not exist is a
|
|
546
|
+
// caller mistake, and answering it with an unfiltered list is the wrong answer delivered
|
|
547
|
+
// confidently. The throw is also what our OR-alias loop needs — a skipped alias would make
|
|
548
|
+
// `a|b` quietly mean `b`.
|
|
549
|
+
const field = conditionField ?? defaultField!
|
|
550
|
+
const preparedCondition = this.clearFieldConditionValue(field, query[key])
|
|
505
551
|
const prepared = this.prepareConditionSql(
|
|
506
|
-
`t1.${
|
|
507
|
-
`
|
|
552
|
+
`t1.${field.alias}`,
|
|
553
|
+
// `conditionIndex` is OURS and upstream still names the parameter after the field alone.
|
|
554
|
+
// Two conditions on one field then bind the same name and the second silently overwrites
|
|
555
|
+
// the first — which is how the documents list lost its filters (f5eb8de).
|
|
556
|
+
`c_1_${conditionIndex}_${field.alias}_value`,
|
|
508
557
|
preparedCondition.value,
|
|
509
558
|
preparedCondition.condition,
|
|
510
559
|
preparedCondition.is_not
|
|
511
560
|
)
|
|
512
561
|
|
|
513
|
-
|
|
562
|
+
anyOf.push(...(prepared.conditions || []))
|
|
514
563
|
Object.assign(replacements, prepared.replacements || {})
|
|
515
564
|
}
|
|
565
|
+
|
|
566
|
+
// Parenthesised as one condition. The caller AND-s the list, so an unwrapped `a or b` would
|
|
567
|
+
// bind looser than the surrounding ANDs and quietly widen every other filter on the query.
|
|
568
|
+
// Upstream pushes the parts straight into `conditions`, which has that effect.
|
|
569
|
+
if (anyOf.length === 1) conditions.push(anyOf[0])
|
|
570
|
+
else if (anyOf.length > 1) conditions.push(`(${anyOf.join(' or ')})`)
|
|
516
571
|
}
|
|
517
572
|
}
|
|
518
573
|
|
package/src/helpers/withes.ts
CHANGED
|
@@ -69,7 +69,7 @@ export class ApiHelpersWithes<T extends CuboApiEntitiesMap<T>> {
|
|
|
69
69
|
|
|
70
70
|
// если нашли кастомное поле
|
|
71
71
|
if (joinCustomField) {
|
|
72
|
-
const relation_entity_id:
|
|
72
|
+
const relation_entity_id: string | undefined = joinCustomField?.extra?.select_entity_id
|
|
73
73
|
const relation_entity_alias: keyof T | undefined = joinCustomField?.extra?.select_entity_alias as keyof T
|
|
74
74
|
|
|
75
75
|
if (!relation_entity_id && !relation_entity_alias) {
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscribing a module backend to its declared hooks (hooks.md §5).
|
|
3
|
+
*
|
|
4
|
+
* A module's hooks are declared on its version, next to the entities they belong to. This is the
|
|
5
|
+
* other half: the running build telling the core "I am alive, I am this version, and these are the
|
|
6
|
+
* tuples I actually implement". The core binds the declarations to this process, works out which
|
|
7
|
+
* accounts that touches — a module backend serves every account it is installed in and deliberately
|
|
8
|
+
* cannot enumerate them — and compiles the chain.
|
|
9
|
+
*
|
|
10
|
+
* What a module backend has to write is one call:
|
|
11
|
+
*
|
|
12
|
+
* const stop = subscribeHooks({
|
|
13
|
+
* apiUrl: process.env.CUBO_API_URL!,
|
|
14
|
+
* token: process.env.CUBO_TOKEN!,
|
|
15
|
+
* version: process.env.CUBO_MODULE_VERSION!,
|
|
16
|
+
* baseUrl: 'http://flowers-be:9100',
|
|
17
|
+
* implemented: tuplesFrom(READS, ENRICH, AFTER, VIRTUAL_FIELDS)
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* and call `stop()` on shutdown. Everything else — the renewal interval, the TTL contract, the boot
|
|
21
|
+
* diff, the graceful unsubscribe — lives here so no module has to reimplement a protocol.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** How often to renew. The core treats a subscription older than 30s as dead, so this has slack. */
|
|
25
|
+
export const SUBSCRIBE_INTERVAL_MS = 10_000
|
|
26
|
+
|
|
27
|
+
export interface SubscribeOptions {
|
|
28
|
+
/** The core API base url, e.g. `http://cubo-api:4000`. */
|
|
29
|
+
apiUrl: string
|
|
30
|
+
/** This service's `cubo-module-service` token — the same one `/auth/me` accepts. */
|
|
31
|
+
token: string
|
|
32
|
+
/** The module VERSION id this build implements. Hooks are a contract with one specific build. */
|
|
33
|
+
version: string
|
|
34
|
+
/**
|
|
35
|
+
* Where this service can be reached, e.g. `http://flowers-be:9100`.
|
|
36
|
+
*
|
|
37
|
+
* Honoured only for the module owner's own account. Everywhere else the core resolves the endpoint
|
|
38
|
+
* from the deployment record instead — the hook url is where the instance sends a live,
|
|
39
|
+
* identity-scoped credential, so it must not be settable by whoever holds this token.
|
|
40
|
+
*/
|
|
41
|
+
baseUrl?: string
|
|
42
|
+
/** The tuples this build implements: `entity:method:phase`. See `hookTuples`. */
|
|
43
|
+
implemented: string[]
|
|
44
|
+
/** Called with the core's boot diff on every successful subscribe. */
|
|
45
|
+
onDiff?: (diff: SubscribeDiff) => void
|
|
46
|
+
/** Called when a renewal fails. Default logs; the loop keeps trying. */
|
|
47
|
+
onError?: (error: unknown) => void
|
|
48
|
+
fetchImpl?: typeof fetch
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SubscribeDiff {
|
|
52
|
+
declared: string[]
|
|
53
|
+
/** Declared but not implemented by this build. */
|
|
54
|
+
missing: string[]
|
|
55
|
+
/** The subset of `missing` the module marked required — every write on those will fail closed. */
|
|
56
|
+
missing_required: string[]
|
|
57
|
+
/** Implemented but never declared. Harmless (the core will not call them) but usually an oversight. */
|
|
58
|
+
undeclared: string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Turn a module's dispatch maps into the tuple list the core diffs against.
|
|
63
|
+
*
|
|
64
|
+
* Module backends key their handlers by `entity:method` and split before/after by route, which is the
|
|
65
|
+
* convention the declaration format was lifted FROM — so the tuples can be derived from the code
|
|
66
|
+
* rather than maintained beside it. Anything derived cannot drift.
|
|
67
|
+
*/
|
|
68
|
+
export function hookTuples(maps: {
|
|
69
|
+
/** Read rewrites — before-phase on the methods each key names. */
|
|
70
|
+
before?: Record<string, unknown>
|
|
71
|
+
/** Response shaping / write effects — after-phase. */
|
|
72
|
+
after?: Record<string, unknown>
|
|
73
|
+
/** Entities whose write bodies are stripped before the engine sees them: create + update, before. */
|
|
74
|
+
virtualFields?: string[] | Record<string, unknown>
|
|
75
|
+
}): string[] {
|
|
76
|
+
const out = new Set<string>()
|
|
77
|
+
for (const key of Object.keys(maps.before ?? {})) out.add(`${key}:before`)
|
|
78
|
+
for (const key of Object.keys(maps.after ?? {})) out.add(`${key}:after`)
|
|
79
|
+
const virtual = Array.isArray(maps.virtualFields) ? maps.virtualFields : Object.keys(maps.virtualFields ?? {})
|
|
80
|
+
for (const entity of virtual) {
|
|
81
|
+
out.add(`${entity}:create:before`)
|
|
82
|
+
out.add(`${entity}:update:before`)
|
|
83
|
+
}
|
|
84
|
+
return [...out].sort()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface Subscription {
|
|
88
|
+
/** Unsubscribe and stop renewing. Safe to call more than once. */
|
|
89
|
+
stop: () => Promise<void>
|
|
90
|
+
/** Force a renewal now — useful in tests, and after a deploy changes what is implemented. */
|
|
91
|
+
renew: () => Promise<SubscribeDiff | null>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Start the subscribe loop. Returns immediately; the first renewal runs in the background so a slow
|
|
96
|
+
* or unreachable core cannot hold up the service's own boot.
|
|
97
|
+
*/
|
|
98
|
+
export function subscribeHooks(options: SubscribeOptions): Subscription {
|
|
99
|
+
const fetchImpl = options.fetchImpl ?? fetch
|
|
100
|
+
const url = `${options.apiUrl.replace(/\/+$/, '')}/modules-api/subscription`
|
|
101
|
+
const headers = { 'content-type': 'application/json', 'cubo-module-service': options.token }
|
|
102
|
+
const onError =
|
|
103
|
+
options.onError ??
|
|
104
|
+
((error: unknown) => {
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.error('[hooks] subscribe failed:', error instanceof Error ? error.message : String(error))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
110
|
+
let stopped = false
|
|
111
|
+
|
|
112
|
+
async function renew(): Promise<SubscribeDiff | null> {
|
|
113
|
+
if (stopped) return null
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetchImpl(url, {
|
|
116
|
+
method: 'PUT',
|
|
117
|
+
headers,
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
version: options.version,
|
|
120
|
+
base_url: options.baseUrl ?? '',
|
|
121
|
+
implemented: options.implemented
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
if (!res.ok) throw new Error(`core replied ${res.status}`)
|
|
125
|
+
const diff = (await res.json()) as SubscribeDiff
|
|
126
|
+
// A required tuple this build does not implement is fatal for those writes — say so loudly at
|
|
127
|
+
// boot rather than letting it surface as an empty `{}` reply at the first save.
|
|
128
|
+
if (diff.missing_required?.length) {
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
console.error(`[hooks] REQUIRED hooks declared but not implemented: ${diff.missing_required.join(', ')}`)
|
|
131
|
+
}
|
|
132
|
+
options.onDiff?.(diff)
|
|
133
|
+
return diff
|
|
134
|
+
} catch (error) {
|
|
135
|
+
onError(error)
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
void renew()
|
|
141
|
+
timer = setInterval(() => void renew(), SUBSCRIBE_INTERVAL_MS)
|
|
142
|
+
// Never hold the process open on our account: a renewal loop is not a reason not to exit.
|
|
143
|
+
timer.unref?.()
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
renew,
|
|
147
|
+
stop: async () => {
|
|
148
|
+
if (stopped) return
|
|
149
|
+
stopped = true
|
|
150
|
+
if (timer) clearInterval(timer)
|
|
151
|
+
timer = null
|
|
152
|
+
try {
|
|
153
|
+
await fetchImpl(url, { method: 'DELETE', headers })
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// A crash-stop is covered by the TTL anyway; failing to say goodbye is not worth an error.
|
|
156
|
+
onError(error)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/hooks/types.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type CuboHookCtx<R, A = unknown> = {
|
|
|
17
17
|
entity: CuboEntity
|
|
18
18
|
req: CuboCrudRequest
|
|
19
19
|
auth?: A
|
|
20
|
-
performer_id?:
|
|
20
|
+
performer_id?: string
|
|
21
21
|
queryOptions: CuboCrudQueryOptions
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -43,8 +43,11 @@ export interface CuboAugmentation<R, A = unknown> {
|
|
|
43
43
|
// авторизация конкретного действия над строкой
|
|
44
44
|
can?(action: CuboCrudAction, ctx: CuboHookCtx<R, A> & { row?: R }): MaybePromise<boolean>
|
|
45
45
|
|
|
46
|
-
// фильтр строки под CRDT
|
|
47
|
-
|
|
46
|
+
// фильтр строки под CRDT-подписку.
|
|
47
|
+
// `auth` — авторизация КЛИЕНТА, владеющего подпиской (адаптер резолвит её по subscribe.client_id).
|
|
48
|
+
// Без неё пуш нельзя ограничить по правам: первичная гидрация подписки идёт через getMany с auth
|
|
49
|
+
// и уважает beforeGetMany, а живые пуши — нет, и строка, скрытая при гидрации, всё равно долетела бы.
|
|
50
|
+
filterRowForCrdtEvent?(row: R, filters: Record<string, any>, auth?: A): boolean
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export type CuboAugmentationsStore<T extends CuboApiEntitiesMap<T>, A = unknown> = {
|
package/src/types/basic.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Transaction } from '@cuboapp/database'
|
|
2
2
|
import { CuboEntity, CuboEntityField } from '@cuboapp/types'
|
|
3
3
|
|
|
4
|
-
import { CUBO_CRUD_ACTION } from '
|
|
4
|
+
import { CUBO_CRUD_ACTION } from '../constants'
|
|
5
5
|
|
|
6
6
|
export type CuboCrudAction = (typeof CUBO_CRUD_ACTION)[number]
|
|
7
7
|
|
|
@@ -27,7 +27,7 @@ export type CuboCrudRequest = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export type CuboCrudOptions = {
|
|
30
|
-
performer_id?:
|
|
30
|
+
performer_id?: string
|
|
31
31
|
transaction?: Transaction
|
|
32
32
|
}
|
|
33
33
|
|
package/src/types/db.ts
CHANGED
|
@@ -78,7 +78,7 @@ export const normalizeCuboCrudQueryOptions = (input?: CuboCrudQueryOptionsInput
|
|
|
78
78
|
|
|
79
79
|
export type CuboCrudMethodOptions<E extends object = {}> = {
|
|
80
80
|
transaction?: Transaction
|
|
81
|
-
performer_id?:
|
|
81
|
+
performer_id?: string
|
|
82
82
|
auth?: any
|
|
83
83
|
extra?: any
|
|
84
84
|
debounce?: number
|
package/.prettierrc
DELETED
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "commonjs",
|
|
4
|
-
"declaration": true,
|
|
5
|
-
"removeComments": true,
|
|
6
|
-
"emitDecoratorMetadata": true,
|
|
7
|
-
"experimentalDecorators": true,
|
|
8
|
-
"allowSyntheticDefaultImports": true,
|
|
9
|
-
"target": "ES2021",
|
|
10
|
-
"sourceMap": true,
|
|
11
|
-
"rootDir": ".",
|
|
12
|
-
"incremental": true,
|
|
13
|
-
"skipLibCheck": true,
|
|
14
|
-
"strict": false,
|
|
15
|
-
"paths": {
|
|
16
|
-
"@/*": ["./src/*"]
|
|
17
|
-
},
|
|
18
|
-
"types": ["node"]
|
|
19
|
-
},
|
|
20
|
-
"include": ["src/**/*"],
|
|
21
|
-
"exclude": ["node_modules", "dist"]
|
|
22
|
-
}
|