@budibase/backend-core 2.9.40-alpha.6 → 2.10.1

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.
Files changed (252) hide show
  1. package/dist/index.js +5 -4
  2. package/dist/index.js.map +2 -2
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +6 -6
  5. package/dist/src/cache/appMetadata.js +1 -1
  6. package/dist/src/cache/appMetadata.js.map +1 -1
  7. package/dist/src/constants/misc.d.ts +0 -2
  8. package/dist/src/constants/misc.js +0 -2
  9. package/dist/src/constants/misc.js.map +1 -1
  10. package/dist/src/environment.js +5 -4
  11. package/dist/src/environment.js.map +1 -1
  12. package/dist/src/logging/system.d.ts +1 -1
  13. package/dist/src/timers/timers.d.ts +1 -1
  14. package/package.json +6 -6
  15. package/src/accounts/accounts.ts +82 -0
  16. package/src/accounts/api.ts +59 -0
  17. package/src/accounts/index.ts +1 -0
  18. package/src/auth/auth.ts +208 -0
  19. package/src/auth/index.ts +1 -0
  20. package/src/auth/tests/auth.spec.ts +14 -0
  21. package/src/blacklist/blacklist.ts +54 -0
  22. package/src/blacklist/index.ts +1 -0
  23. package/src/blacklist/tests/blacklist.spec.ts +46 -0
  24. package/src/cache/appMetadata.ts +88 -0
  25. package/src/cache/base/index.ts +92 -0
  26. package/src/cache/generic.ts +30 -0
  27. package/src/cache/index.ts +5 -0
  28. package/src/cache/tests/writethrough.spec.ts +138 -0
  29. package/src/cache/user.ts +83 -0
  30. package/src/cache/writethrough.ts +133 -0
  31. package/src/configs/configs.ts +257 -0
  32. package/src/configs/index.ts +1 -0
  33. package/src/configs/tests/configs.spec.ts +184 -0
  34. package/src/constants/db.ts +63 -0
  35. package/src/constants/index.ts +2 -0
  36. package/src/constants/misc.ts +50 -0
  37. package/src/context/Context.ts +14 -0
  38. package/src/context/identity.ts +58 -0
  39. package/src/context/index.ts +3 -0
  40. package/src/context/mainContext.ts +310 -0
  41. package/src/context/tests/index.spec.ts +147 -0
  42. package/src/context/types.ts +11 -0
  43. package/src/db/Replication.ts +84 -0
  44. package/src/db/constants.ts +10 -0
  45. package/src/db/couch/DatabaseImpl.ts +238 -0
  46. package/src/db/couch/connections.ts +77 -0
  47. package/src/db/couch/index.ts +5 -0
  48. package/src/db/couch/pouchDB.ts +97 -0
  49. package/src/db/couch/pouchDump.ts +0 -0
  50. package/src/db/couch/utils.ts +50 -0
  51. package/src/db/db.ts +43 -0
  52. package/src/db/errors.ts +14 -0
  53. package/src/db/index.ts +12 -0
  54. package/src/db/lucene.ts +750 -0
  55. package/src/db/searchIndexes/index.ts +1 -0
  56. package/src/db/searchIndexes/searchIndexes.ts +62 -0
  57. package/src/db/tests/index.spec.js +25 -0
  58. package/src/db/tests/lucene.spec.ts +368 -0
  59. package/src/db/tests/pouch.spec.js +62 -0
  60. package/src/db/tests/utils.spec.ts +63 -0
  61. package/src/db/utils.ts +207 -0
  62. package/src/db/views.ts +241 -0
  63. package/src/docIds/conversions.ts +59 -0
  64. package/src/docIds/ids.ts +113 -0
  65. package/src/docIds/index.ts +2 -0
  66. package/src/docIds/newid.ts +5 -0
  67. package/src/docIds/params.ts +174 -0
  68. package/src/docUpdates/index.ts +29 -0
  69. package/src/environment.ts +201 -0
  70. package/src/errors/errors.ts +119 -0
  71. package/src/errors/index.ts +1 -0
  72. package/src/events/analytics.ts +6 -0
  73. package/src/events/asyncEvents/index.ts +2 -0
  74. package/src/events/asyncEvents/publisher.ts +12 -0
  75. package/src/events/asyncEvents/queue.ts +22 -0
  76. package/src/events/backfill.ts +183 -0
  77. package/src/events/documentId.ts +56 -0
  78. package/src/events/events.ts +40 -0
  79. package/src/events/identification.ts +310 -0
  80. package/src/events/index.ts +14 -0
  81. package/src/events/processors/AnalyticsProcessor.ts +64 -0
  82. package/src/events/processors/AuditLogsProcessor.ts +93 -0
  83. package/src/events/processors/LoggingProcessor.ts +37 -0
  84. package/src/events/processors/Processors.ts +52 -0
  85. package/src/events/processors/async/DocumentUpdateProcessor.ts +43 -0
  86. package/src/events/processors/index.ts +19 -0
  87. package/src/events/processors/posthog/PosthogProcessor.ts +118 -0
  88. package/src/events/processors/posthog/index.ts +2 -0
  89. package/src/events/processors/posthog/rateLimiting.ts +106 -0
  90. package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +168 -0
  91. package/src/events/processors/types.ts +1 -0
  92. package/src/events/publishers/account.ts +35 -0
  93. package/src/events/publishers/app.ts +155 -0
  94. package/src/events/publishers/auditLog.ts +26 -0
  95. package/src/events/publishers/auth.ts +73 -0
  96. package/src/events/publishers/automation.ts +110 -0
  97. package/src/events/publishers/backfill.ts +74 -0
  98. package/src/events/publishers/backup.ts +42 -0
  99. package/src/events/publishers/datasource.ts +48 -0
  100. package/src/events/publishers/email.ts +17 -0
  101. package/src/events/publishers/environmentVariable.ts +38 -0
  102. package/src/events/publishers/group.ts +99 -0
  103. package/src/events/publishers/index.ts +24 -0
  104. package/src/events/publishers/installation.ts +38 -0
  105. package/src/events/publishers/layout.ts +26 -0
  106. package/src/events/publishers/license.ts +84 -0
  107. package/src/events/publishers/org.ts +37 -0
  108. package/src/events/publishers/plugin.ts +47 -0
  109. package/src/events/publishers/query.ts +88 -0
  110. package/src/events/publishers/role.ts +62 -0
  111. package/src/events/publishers/rows.ts +29 -0
  112. package/src/events/publishers/screen.ts +36 -0
  113. package/src/events/publishers/serve.ts +43 -0
  114. package/src/events/publishers/table.ts +70 -0
  115. package/src/events/publishers/user.ts +202 -0
  116. package/src/events/publishers/view.ts +107 -0
  117. package/src/features/index.ts +78 -0
  118. package/src/features/installation.ts +17 -0
  119. package/src/features/tests/featureFlags.spec.ts +85 -0
  120. package/src/helpers.ts +9 -0
  121. package/src/index.ts +54 -0
  122. package/src/installation.ts +107 -0
  123. package/src/logging/alerts.ts +26 -0
  124. package/src/logging/correlation/correlation.ts +13 -0
  125. package/src/logging/correlation/index.ts +1 -0
  126. package/src/logging/correlation/middleware.ts +17 -0
  127. package/src/logging/index.ts +4 -0
  128. package/src/logging/pino/logger.ts +232 -0
  129. package/src/logging/pino/middleware.ts +45 -0
  130. package/src/logging/system.ts +81 -0
  131. package/src/logging/tests/system.spec.ts +61 -0
  132. package/src/middleware/adminOnly.ts +9 -0
  133. package/src/middleware/auditLog.ts +6 -0
  134. package/src/middleware/authenticated.ts +193 -0
  135. package/src/middleware/builderOnly.ts +21 -0
  136. package/src/middleware/builderOrAdmin.ts +21 -0
  137. package/src/middleware/csrf.ts +81 -0
  138. package/src/middleware/errorHandling.ts +29 -0
  139. package/src/middleware/index.ts +21 -0
  140. package/src/middleware/internalApi.ts +23 -0
  141. package/src/middleware/joi-validator.ts +45 -0
  142. package/src/middleware/matchers.ts +47 -0
  143. package/src/middleware/passport/datasource/google.ts +95 -0
  144. package/src/middleware/passport/local.ts +54 -0
  145. package/src/middleware/passport/sso/google.ts +77 -0
  146. package/src/middleware/passport/sso/oidc.ts +154 -0
  147. package/src/middleware/passport/sso/sso.ts +165 -0
  148. package/src/middleware/passport/sso/tests/google.spec.ts +67 -0
  149. package/src/middleware/passport/sso/tests/oidc.spec.ts +152 -0
  150. package/src/middleware/passport/sso/tests/sso.spec.ts +197 -0
  151. package/src/middleware/passport/utils.ts +38 -0
  152. package/src/middleware/querystringToBody.ts +28 -0
  153. package/src/middleware/tenancy.ts +36 -0
  154. package/src/middleware/tests/builder.spec.ts +180 -0
  155. package/src/middleware/tests/matchers.spec.ts +134 -0
  156. package/src/migrations/definitions.ts +40 -0
  157. package/src/migrations/index.ts +2 -0
  158. package/src/migrations/migrations.ts +191 -0
  159. package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +11 -0
  160. package/src/migrations/tests/migrations.spec.ts +64 -0
  161. package/src/objectStore/buckets/app.ts +40 -0
  162. package/src/objectStore/buckets/global.ts +29 -0
  163. package/src/objectStore/buckets/index.ts +3 -0
  164. package/src/objectStore/buckets/plugins.ts +71 -0
  165. package/src/objectStore/buckets/tests/app.spec.ts +171 -0
  166. package/src/objectStore/buckets/tests/global.spec.ts +74 -0
  167. package/src/objectStore/buckets/tests/plugins.spec.ts +111 -0
  168. package/src/objectStore/cloudfront.ts +41 -0
  169. package/src/objectStore/index.ts +3 -0
  170. package/src/objectStore/objectStore.ts +440 -0
  171. package/src/objectStore/utils.ts +27 -0
  172. package/src/platform/index.ts +3 -0
  173. package/src/platform/platformDb.ts +6 -0
  174. package/src/platform/tenants.ts +101 -0
  175. package/src/platform/tests/tenants.spec.ts +26 -0
  176. package/src/platform/users.ts +90 -0
  177. package/src/plugin/index.ts +1 -0
  178. package/src/plugin/tests/validation.spec.ts +83 -0
  179. package/src/plugin/utils.ts +156 -0
  180. package/src/queue/constants.ts +6 -0
  181. package/src/queue/inMemoryQueue.ts +141 -0
  182. package/src/queue/index.ts +2 -0
  183. package/src/queue/listeners.ts +195 -0
  184. package/src/queue/queue.ts +54 -0
  185. package/src/redis/index.ts +6 -0
  186. package/src/redis/init.ts +86 -0
  187. package/src/redis/redis.ts +308 -0
  188. package/src/redis/redlockImpl.ts +139 -0
  189. package/src/redis/utils.ts +117 -0
  190. package/src/security/encryption.ts +179 -0
  191. package/src/security/permissions.ts +158 -0
  192. package/src/security/roles.ts +389 -0
  193. package/src/security/sessions.ts +120 -0
  194. package/src/security/tests/encryption.spec.ts +31 -0
  195. package/src/security/tests/permissions.spec.ts +145 -0
  196. package/src/security/tests/sessions.spec.ts +12 -0
  197. package/src/tenancy/db.ts +6 -0
  198. package/src/tenancy/index.ts +2 -0
  199. package/src/tenancy/tenancy.ts +140 -0
  200. package/src/tenancy/tests/tenancy.spec.ts +184 -0
  201. package/src/timers/index.ts +1 -0
  202. package/src/timers/timers.ts +22 -0
  203. package/src/users/db.ts +484 -0
  204. package/src/users/events.ts +176 -0
  205. package/src/users/index.ts +4 -0
  206. package/src/users/lookup.ts +102 -0
  207. package/src/users/users.ts +276 -0
  208. package/src/users/utils.ts +55 -0
  209. package/src/utils/hashing.ts +14 -0
  210. package/src/utils/index.ts +3 -0
  211. package/src/utils/stringUtils.ts +8 -0
  212. package/src/utils/tests/utils.spec.ts +191 -0
  213. package/src/utils/utils.ts +239 -0
  214. package/tests/core/logging.ts +34 -0
  215. package/tests/core/utilities/index.ts +6 -0
  216. package/tests/core/utilities/jestUtils.ts +30 -0
  217. package/tests/core/utilities/mocks/alerts.ts +3 -0
  218. package/tests/core/utilities/mocks/date.ts +2 -0
  219. package/tests/core/utilities/mocks/events.ts +131 -0
  220. package/tests/core/utilities/mocks/fetch.ts +17 -0
  221. package/tests/core/utilities/mocks/index.ts +10 -0
  222. package/tests/core/utilities/mocks/licenses.ts +115 -0
  223. package/tests/core/utilities/mocks/posthog.ts +7 -0
  224. package/tests/core/utilities/structures/Chance.ts +20 -0
  225. package/tests/core/utilities/structures/accounts.ts +115 -0
  226. package/tests/core/utilities/structures/apps.ts +21 -0
  227. package/tests/core/utilities/structures/common.ts +7 -0
  228. package/tests/core/utilities/structures/db.ts +12 -0
  229. package/tests/core/utilities/structures/documents/index.ts +1 -0
  230. package/tests/core/utilities/structures/documents/platform/index.ts +1 -0
  231. package/tests/core/utilities/structures/documents/platform/installation.ts +12 -0
  232. package/tests/core/utilities/structures/generator.ts +2 -0
  233. package/tests/core/utilities/structures/index.ts +15 -0
  234. package/tests/core/utilities/structures/koa.ts +16 -0
  235. package/tests/core/utilities/structures/licenses.ts +167 -0
  236. package/tests/core/utilities/structures/plugins.ts +19 -0
  237. package/tests/core/utilities/structures/quotas.ts +67 -0
  238. package/tests/core/utilities/structures/scim.ts +80 -0
  239. package/tests/core/utilities/structures/shared.ts +19 -0
  240. package/tests/core/utilities/structures/sso.ts +119 -0
  241. package/tests/core/utilities/structures/tenants.ts +5 -0
  242. package/tests/core/utilities/structures/userGroups.ts +10 -0
  243. package/tests/core/utilities/structures/users.ts +73 -0
  244. package/tests/core/utilities/testContainerUtils.ts +85 -0
  245. package/tests/core/utilities/utils/index.ts +1 -0
  246. package/tests/core/utilities/utils/time.ts +3 -0
  247. package/tests/extra/DBTestConfiguration.ts +36 -0
  248. package/tests/extra/index.ts +2 -0
  249. package/tests/extra/testEnv.ts +95 -0
  250. package/tests/index.ts +1 -0
  251. package/tests/jestEnv.ts +6 -0
  252. package/tests/jestSetup.ts +28 -0
@@ -0,0 +1,750 @@
1
+ import fetch from "node-fetch"
2
+ import { getCouchInfo } from "./couch"
3
+ import { SearchFilters, Row, EmptyFilterOption } from "@budibase/types"
4
+
5
+ const QUERY_START_REGEX = /\d[0-9]*:/g
6
+
7
+ interface SearchResponse<T> {
8
+ rows: T[] | any[]
9
+ bookmark?: string
10
+ totalRows: number
11
+ }
12
+
13
+ interface PaginatedSearchResponse<T> extends SearchResponse<T> {
14
+ hasNextPage: boolean
15
+ }
16
+
17
+ export type SearchParams<T> = {
18
+ tableId?: string
19
+ sort?: string
20
+ sortOrder?: string
21
+ sortType?: string
22
+ limit?: number
23
+ bookmark?: string
24
+ version?: string
25
+ indexer?: () => Promise<any>
26
+ disableEscaping?: boolean
27
+ rows?: T | Row[]
28
+ }
29
+
30
+ export function removeKeyNumbering(key: any): string {
31
+ if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) {
32
+ const parts = key.split(":")
33
+ // remove the number
34
+ parts.shift()
35
+ return parts.join(":")
36
+ } else {
37
+ return key
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Class to build lucene query URLs.
43
+ * Optionally takes a base lucene query object.
44
+ */
45
+ export class QueryBuilder<T> {
46
+ #dbName: string
47
+ #index: string
48
+ #query: SearchFilters
49
+ #limit: number
50
+ #sort?: string
51
+ #bookmark?: string
52
+ #sortOrder: string
53
+ #sortType: string
54
+ #includeDocs: boolean
55
+ #version?: string
56
+ #indexBuilder?: () => Promise<any>
57
+ #noEscaping = false
58
+ #skip?: number
59
+
60
+ static readonly maxLimit = 200
61
+
62
+ constructor(dbName: string, index: string, base?: SearchFilters) {
63
+ this.#dbName = dbName
64
+ this.#index = index
65
+ this.#query = {
66
+ allOr: false,
67
+ onEmptyFilter: EmptyFilterOption.RETURN_ALL,
68
+ string: {},
69
+ fuzzy: {},
70
+ range: {},
71
+ equal: {},
72
+ notEqual: {},
73
+ empty: {},
74
+ notEmpty: {},
75
+ oneOf: {},
76
+ contains: {},
77
+ notContains: {},
78
+ containsAny: {},
79
+ ...base,
80
+ }
81
+ this.#limit = 50
82
+ this.#sortOrder = "ascending"
83
+ this.#sortType = "string"
84
+ this.#includeDocs = true
85
+ }
86
+
87
+ disableEscaping() {
88
+ this.#noEscaping = true
89
+ return this
90
+ }
91
+
92
+ setIndexBuilder(builderFn: () => Promise<any>) {
93
+ this.#indexBuilder = builderFn
94
+ return this
95
+ }
96
+
97
+ setVersion(version?: string) {
98
+ if (version != null) {
99
+ this.#version = version
100
+ }
101
+ return this
102
+ }
103
+
104
+ setTable(tableId: string) {
105
+ this.#query.equal!.tableId = tableId
106
+ return this
107
+ }
108
+
109
+ setLimit(limit?: number) {
110
+ if (limit != null) {
111
+ this.#limit = limit
112
+ }
113
+ return this
114
+ }
115
+
116
+ setSort(sort?: string) {
117
+ if (sort != null) {
118
+ this.#sort = sort
119
+ }
120
+ return this
121
+ }
122
+
123
+ setSortOrder(sortOrder?: string) {
124
+ if (sortOrder != null) {
125
+ this.#sortOrder = sortOrder
126
+ }
127
+ return this
128
+ }
129
+
130
+ setSortType(sortType?: string) {
131
+ if (sortType != null) {
132
+ this.#sortType = sortType
133
+ }
134
+ return this
135
+ }
136
+
137
+ setBookmark(bookmark?: string) {
138
+ if (bookmark != null) {
139
+ this.#bookmark = bookmark
140
+ }
141
+ return this
142
+ }
143
+
144
+ setSkip(skip: number | undefined) {
145
+ this.#skip = skip
146
+ return this
147
+ }
148
+
149
+ excludeDocs() {
150
+ this.#includeDocs = false
151
+ return this
152
+ }
153
+
154
+ includeDocs() {
155
+ this.#includeDocs = true
156
+ return this
157
+ }
158
+
159
+ addString(key: string, partial: string) {
160
+ this.#query.string![key] = partial
161
+ return this
162
+ }
163
+
164
+ addFuzzy(key: string, fuzzy: string) {
165
+ this.#query.fuzzy![key] = fuzzy
166
+ return this
167
+ }
168
+
169
+ addRange(key: string, low: string | number, high: string | number) {
170
+ this.#query.range![key] = {
171
+ low,
172
+ high,
173
+ }
174
+ return this
175
+ }
176
+
177
+ addEqual(key: string, value: any) {
178
+ this.#query.equal![key] = value
179
+ return this
180
+ }
181
+
182
+ addNotEqual(key: string, value: any) {
183
+ this.#query.notEqual![key] = value
184
+ return this
185
+ }
186
+
187
+ addEmpty(key: string, value: any) {
188
+ this.#query.empty![key] = value
189
+ return this
190
+ }
191
+
192
+ addNotEmpty(key: string, value: any) {
193
+ this.#query.notEmpty![key] = value
194
+ return this
195
+ }
196
+
197
+ addOneOf(key: string, value: any) {
198
+ this.#query.oneOf![key] = value
199
+ return this
200
+ }
201
+
202
+ addContains(key: string, value: any) {
203
+ this.#query.contains![key] = value
204
+ return this
205
+ }
206
+
207
+ addNotContains(key: string, value: any) {
208
+ this.#query.notContains![key] = value
209
+ return this
210
+ }
211
+
212
+ addContainsAny(key: string, value: any) {
213
+ this.#query.containsAny![key] = value
214
+ return this
215
+ }
216
+
217
+ setAllOr() {
218
+ this.#query.allOr = true
219
+ }
220
+
221
+ setOnEmptyFilter(value: EmptyFilterOption) {
222
+ this.#query.onEmptyFilter = value
223
+ }
224
+
225
+ handleSpaces(input: string) {
226
+ if (this.#noEscaping) {
227
+ return input
228
+ } else {
229
+ return input.replace(/ /g, "_")
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Preprocesses a value before going into a lucene search.
235
+ * Transforms strings to lowercase and wraps strings and bools in quotes.
236
+ * @param value The value to process
237
+ * @param options The preprocess options
238
+ * @returns {string|*}
239
+ */
240
+ preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) {
241
+ const hasVersion = !!this.#version
242
+ // Determine if type needs wrapped
243
+ const originalType = typeof value
244
+ // Convert to lowercase
245
+ if (value && lowercase) {
246
+ value = value.toLowerCase ? value.toLowerCase() : value
247
+ }
248
+ // Escape characters
249
+ if (!this.#noEscaping && escape && originalType === "string") {
250
+ value = `${value}`.replace(/[ \/#+\-&|!(){}\]^"~*?:\\]/g, "\\$&")
251
+ }
252
+
253
+ // Wrap in quotes
254
+ if (originalType === "string" && !isNaN(value) && !type) {
255
+ value = `"${value}"`
256
+ } else if (hasVersion && wrap) {
257
+ value = originalType === "number" ? value : `"${value}"`
258
+ }
259
+ return value
260
+ }
261
+
262
+ isMultiCondition() {
263
+ let count = 0
264
+ for (let filters of Object.values(this.#query)) {
265
+ // not contains is one massive filter in allOr mode
266
+ if (typeof filters === "object") {
267
+ count += Object.keys(filters).length
268
+ }
269
+ }
270
+ return count > 1
271
+ }
272
+
273
+ compressFilters(filters: Record<string, string[]>) {
274
+ const compressed: typeof filters = {}
275
+ for (let key of Object.keys(filters)) {
276
+ const finalKey = removeKeyNumbering(key)
277
+ if (compressed[finalKey]) {
278
+ compressed[finalKey] = compressed[finalKey].concat(filters[key])
279
+ } else {
280
+ compressed[finalKey] = filters[key]
281
+ }
282
+ }
283
+ // add prefixes back
284
+ const final: typeof filters = {}
285
+ let count = 1
286
+ for (let [key, value] of Object.entries(compressed)) {
287
+ final[`${count++}:${key}`] = value
288
+ }
289
+ return final
290
+ }
291
+
292
+ buildSearchQuery() {
293
+ const builder = this
294
+ let allOr = this.#query && this.#query.allOr
295
+ let query = allOr ? "" : "*:*"
296
+ let allFiltersEmpty = true
297
+ const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true }
298
+ let tableId: string = ""
299
+ if (this.#query.equal!.tableId) {
300
+ tableId = this.#query.equal!.tableId
301
+ delete this.#query.equal!.tableId
302
+ }
303
+
304
+ const equal = (key: string, value: any) => {
305
+ // 0 evaluates to false, which means we would return all rows if we don't check it
306
+ if (!value && value !== 0) {
307
+ return null
308
+ }
309
+ return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
310
+ }
311
+
312
+ const contains = (key: string, value: any, mode = "AND") => {
313
+ if (!value || (Array.isArray(value) && value.length === 0)) {
314
+ return null
315
+ }
316
+ if (!Array.isArray(value)) {
317
+ return `${key}:${value}`
318
+ }
319
+ let statement = `${builder.preprocess(value[0], { escape: true })}`
320
+ for (let i = 1; i < value.length; i++) {
321
+ statement += ` ${mode} ${builder.preprocess(value[i], {
322
+ escape: true,
323
+ })}`
324
+ }
325
+ return `${key}:(${statement})`
326
+ }
327
+
328
+ const fuzzy = (key: string, value: any) => {
329
+ if (!value) {
330
+ return null
331
+ }
332
+ value = builder.preprocess(value, {
333
+ escape: true,
334
+ lowercase: true,
335
+ type: "fuzzy",
336
+ })
337
+ return `${key}:/.*${value}.*/`
338
+ }
339
+
340
+ const notContains = (key: string, value: any) => {
341
+ const allPrefix = allOr ? "*:* AND " : ""
342
+ const mode = allOr ? "AND" : undefined
343
+ return allPrefix + "NOT " + contains(key, value, mode)
344
+ }
345
+
346
+ const containsAny = (key: string, value: any) => {
347
+ return contains(key, value, "OR")
348
+ }
349
+
350
+ const oneOf = (key: string, value: any) => {
351
+ if (!value) {
352
+ return `*:*`
353
+ }
354
+ if (!Array.isArray(value)) {
355
+ if (typeof value === "string") {
356
+ value = value.split(",")
357
+ } else {
358
+ return ""
359
+ }
360
+ }
361
+ let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}`
362
+ for (let i = 1; i < value.length; i++) {
363
+ orStatement += ` OR ${builder.preprocess(
364
+ value[i],
365
+ allPreProcessingOpts
366
+ )}`
367
+ }
368
+ return `${key}:(${orStatement})`
369
+ }
370
+
371
+ function build(
372
+ structure: any,
373
+ queryFn: (key: string, value: any) => string | null,
374
+ opts?: { returnBuilt?: boolean; mode?: string }
375
+ ) {
376
+ let built = ""
377
+ for (let [key, value] of Object.entries(structure)) {
378
+ // check for new format - remove numbering if needed
379
+ key = removeKeyNumbering(key)
380
+ key = builder.preprocess(builder.handleSpaces(key), {
381
+ escape: true,
382
+ })
383
+ let expression = queryFn(key, value)
384
+ if (expression == null) {
385
+ continue
386
+ }
387
+ if (built.length > 0 || query.length > 0) {
388
+ const mode = opts?.mode ? opts.mode : allOr ? "OR" : "AND"
389
+ built += ` ${mode} `
390
+ }
391
+ built += expression
392
+ if (
393
+ (typeof value !== "string" && value != null) ||
394
+ (typeof value === "string" && value !== tableId && value !== "")
395
+ ) {
396
+ allFiltersEmpty = false
397
+ }
398
+ }
399
+ if (opts?.returnBuilt) {
400
+ return built
401
+ } else {
402
+ query += built
403
+ }
404
+ }
405
+
406
+ // Construct the actual lucene search query string from JSON structure
407
+ if (this.#query.string) {
408
+ build(this.#query.string, (key: string, value: any) => {
409
+ if (!value) {
410
+ return null
411
+ }
412
+ value = builder.preprocess(value, {
413
+ escape: true,
414
+ lowercase: true,
415
+ type: "string",
416
+ })
417
+ return `${key}:${value}*`
418
+ })
419
+ }
420
+ if (this.#query.range) {
421
+ build(this.#query.range, (key: string, value: any) => {
422
+ if (!value) {
423
+ return null
424
+ }
425
+ if (value.low == null || value.low === "") {
426
+ return null
427
+ }
428
+ if (value.high == null || value.high === "") {
429
+ return null
430
+ }
431
+ const low = builder.preprocess(value.low, allPreProcessingOpts)
432
+ const high = builder.preprocess(value.high, allPreProcessingOpts)
433
+ return `${key}:[${low} TO ${high}]`
434
+ })
435
+ }
436
+ if (this.#query.fuzzy) {
437
+ build(this.#query.fuzzy, fuzzy)
438
+ }
439
+ if (this.#query.equal) {
440
+ build(this.#query.equal, equal)
441
+ }
442
+ if (this.#query.notEqual) {
443
+ build(this.#query.notEqual, (key: string, value: any) => {
444
+ if (!value) {
445
+ return null
446
+ }
447
+ if (typeof value === "boolean") {
448
+ return `(*:* AND !${key}:${value})`
449
+ }
450
+ return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
451
+ })
452
+ }
453
+ if (this.#query.empty) {
454
+ build(this.#query.empty, (key: string) => `(*:* -${key}:["" TO *])`)
455
+ }
456
+ if (this.#query.notEmpty) {
457
+ build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`)
458
+ }
459
+ if (this.#query.oneOf) {
460
+ build(this.#query.oneOf, oneOf)
461
+ }
462
+ if (this.#query.contains) {
463
+ build(this.#query.contains, contains)
464
+ }
465
+ if (this.#query.notContains) {
466
+ build(this.compressFilters(this.#query.notContains), notContains)
467
+ }
468
+ if (this.#query.containsAny) {
469
+ build(this.#query.containsAny, containsAny)
470
+ }
471
+ // make sure table ID is always added as an AND
472
+ if (tableId) {
473
+ query = this.isMultiCondition() ? `(${query})` : query
474
+ allOr = false
475
+ build({ tableId }, equal)
476
+ }
477
+ if (allFiltersEmpty) {
478
+ if (this.#query.onEmptyFilter === EmptyFilterOption.RETURN_NONE) {
479
+ return ""
480
+ } else if (this.#query?.allOr) {
481
+ return query.replace("()", "(*:*)")
482
+ }
483
+ }
484
+ return query
485
+ }
486
+
487
+ buildSearchBody() {
488
+ let body: any = {
489
+ q: this.buildSearchQuery(),
490
+ limit: Math.min(this.#limit, QueryBuilder.maxLimit),
491
+ include_docs: this.#includeDocs,
492
+ }
493
+ if (this.#bookmark) {
494
+ body.bookmark = this.#bookmark
495
+ }
496
+ if (this.#sort) {
497
+ const order = this.#sortOrder === "descending" ? "-" : ""
498
+ const type = `<${this.#sortType}>`
499
+ body.sort = `${order}${this.handleSpaces(this.#sort)}${type}`
500
+ }
501
+ return body
502
+ }
503
+
504
+ async run() {
505
+ if (this.#skip) {
506
+ await this.#skipItems(this.#skip)
507
+ }
508
+ return await this.#execute()
509
+ }
510
+
511
+ /**
512
+ * Lucene queries do not support pagination and use bookmarks instead.
513
+ * For the given builder, walk through pages using bookmarks until the desired
514
+ * page has been met.
515
+ */
516
+ async #skipItems(skip: number) {
517
+ // Lucene does not support pagination.
518
+ // Handle pagination by finding the right bookmark
519
+ const prevIncludeDocs = this.#includeDocs
520
+ const prevLimit = this.#limit
521
+
522
+ this.excludeDocs()
523
+ let skipRemaining = skip
524
+ let iterationFetched = 0
525
+ do {
526
+ const toSkip = Math.min(QueryBuilder.maxLimit, skipRemaining)
527
+ this.setLimit(toSkip)
528
+ const { bookmark, rows } = await this.#execute()
529
+ this.setBookmark(bookmark)
530
+ iterationFetched = rows.length
531
+ skipRemaining -= rows.length
532
+ } while (skipRemaining > 0 && iterationFetched > 0)
533
+
534
+ this.#includeDocs = prevIncludeDocs
535
+ this.#limit = prevLimit
536
+ }
537
+
538
+ async #execute() {
539
+ const { url, cookie } = getCouchInfo()
540
+ const fullPath = `${url}/${this.#dbName}/_design/database/_search/${
541
+ this.#index
542
+ }`
543
+ const body = this.buildSearchBody()
544
+ try {
545
+ return await runQuery<T>(fullPath, body, cookie)
546
+ } catch (err: any) {
547
+ if (err.status === 404 && this.#indexBuilder) {
548
+ await this.#indexBuilder()
549
+ return await runQuery<T>(fullPath, body, cookie)
550
+ } else {
551
+ throw err
552
+ }
553
+ }
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Executes a lucene search query.
559
+ * @param url The query URL
560
+ * @param body The request body defining search criteria
561
+ * @param cookie The auth cookie for CouchDB
562
+ * @returns {Promise<{rows: []}>}
563
+ */
564
+ async function runQuery<T>(
565
+ url: string,
566
+ body: any,
567
+ cookie: string
568
+ ): Promise<SearchResponse<T>> {
569
+ const response = await fetch(url, {
570
+ body: JSON.stringify(body),
571
+ method: "POST",
572
+ headers: {
573
+ Authorization: cookie,
574
+ },
575
+ })
576
+
577
+ if (response.status === 404) {
578
+ throw response
579
+ }
580
+ const json = await response.json()
581
+
582
+ let output: SearchResponse<T> = {
583
+ rows: [],
584
+ totalRows: 0,
585
+ }
586
+ if (json.rows != null && json.rows.length > 0) {
587
+ output.rows = json.rows.map((row: any) => row.doc)
588
+ }
589
+ if (json.bookmark) {
590
+ output.bookmark = json.bookmark
591
+ }
592
+ if (json.total_rows) {
593
+ output.totalRows = json.total_rows
594
+ }
595
+ return output
596
+ }
597
+
598
+ /**
599
+ * Gets round the fixed limit of 200 results from a query by fetching as many
600
+ * pages as required and concatenating the results. This recursively operates
601
+ * until enough results have been found.
602
+ * @param dbName {string} Which database to run a lucene query on
603
+ * @param index {string} Which search index to utilise
604
+ * @param query {object} The JSON query structure
605
+ * @param params {object} The search params including:
606
+ * tableId {string} The table ID to search
607
+ * sort {string} The sort column
608
+ * sortOrder {string} The sort order ("ascending" or "descending")
609
+ * sortType {string} Whether to treat sortable values as strings or
610
+ * numbers. ("string" or "number")
611
+ * limit {number} The number of results to fetch
612
+ * bookmark {string|null} Current bookmark in the recursive search
613
+ * rows {array|null} Current results in the recursive search
614
+ * @returns {Promise<*[]|*>}
615
+ */
616
+ async function recursiveSearch<T>(
617
+ dbName: string,
618
+ index: string,
619
+ query: any,
620
+ params: any
621
+ ): Promise<any> {
622
+ const bookmark = params.bookmark
623
+ const rows = params.rows || []
624
+ if (rows.length >= params.limit) {
625
+ return rows
626
+ }
627
+ let pageSize = QueryBuilder.maxLimit
628
+ if (rows.length > params.limit - QueryBuilder.maxLimit) {
629
+ pageSize = params.limit - rows.length
630
+ }
631
+ const page = await new QueryBuilder<T>(dbName, index, query)
632
+ .setVersion(params.version)
633
+ .setTable(params.tableId)
634
+ .setBookmark(bookmark)
635
+ .setLimit(pageSize)
636
+ .setSort(params.sort)
637
+ .setSortOrder(params.sortOrder)
638
+ .setSortType(params.sortType)
639
+ .run()
640
+ if (!page.rows.length) {
641
+ return rows
642
+ }
643
+ if (page.rows.length < QueryBuilder.maxLimit) {
644
+ return [...rows, ...page.rows]
645
+ }
646
+ const newParams = {
647
+ ...params,
648
+ bookmark: page.bookmark,
649
+ rows: [...rows, ...page.rows],
650
+ }
651
+ return await recursiveSearch(dbName, index, query, newParams)
652
+ }
653
+
654
+ /**
655
+ * Performs a paginated search. A bookmark will be returned to allow the next
656
+ * page to be fetched. There is a max limit off 200 results per page in a
657
+ * paginated search.
658
+ * @param dbName {string} Which database to run a lucene query on
659
+ * @param index {string} Which search index to utilise
660
+ * @param query {object} The JSON query structure
661
+ * @param params {object} The search params including:
662
+ * tableId {string} The table ID to search
663
+ * sort {string} The sort column
664
+ * sortOrder {string} The sort order ("ascending" or "descending")
665
+ * sortType {string} Whether to treat sortable values as strings or
666
+ * numbers. ("string" or "number")
667
+ * limit {number} The desired page size
668
+ * bookmark {string} The bookmark to resume from
669
+ * @returns {Promise<{hasNextPage: boolean, rows: *[]}>}
670
+ */
671
+ export async function paginatedSearch<T>(
672
+ dbName: string,
673
+ index: string,
674
+ query: SearchFilters,
675
+ params: SearchParams<T>
676
+ ) {
677
+ let limit = params.limit
678
+ if (limit == null || isNaN(limit) || limit < 0) {
679
+ limit = 50
680
+ }
681
+ limit = Math.min(limit, QueryBuilder.maxLimit)
682
+ const search = new QueryBuilder<T>(dbName, index, query)
683
+ if (params.version) {
684
+ search.setVersion(params.version)
685
+ }
686
+ if (params.tableId) {
687
+ search.setTable(params.tableId)
688
+ }
689
+ if (params.sort) {
690
+ search
691
+ .setSort(params.sort)
692
+ .setSortOrder(params.sortOrder)
693
+ .setSortType(params.sortType)
694
+ }
695
+ if (params.indexer) {
696
+ search.setIndexBuilder(params.indexer)
697
+ }
698
+ if (params.disableEscaping) {
699
+ search.disableEscaping()
700
+ }
701
+ const searchResults = await search
702
+ .setBookmark(params.bookmark)
703
+ .setLimit(limit)
704
+ .run()
705
+
706
+ // Try fetching 1 row in the next page to see if another page of results
707
+ // exists or not
708
+ search.setBookmark(searchResults.bookmark).setLimit(1)
709
+ if (params.tableId) {
710
+ search.setTable(params.tableId)
711
+ }
712
+ const nextResults = await search.run()
713
+
714
+ return {
715
+ ...searchResults,
716
+ hasNextPage: nextResults.rows && nextResults.rows.length > 0,
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Performs a full search, fetching multiple pages if required to return the
722
+ * desired amount of results. There is a limit of 1000 results to avoid
723
+ * heavy performance hits, and to avoid client components breaking from
724
+ * handling too much data.
725
+ * @param dbName {string} Which database to run a lucene query on
726
+ * @param index {string} Which search index to utilise
727
+ * @param query {object} The JSON query structure
728
+ * @param params {object} The search params including:
729
+ * tableId {string} The table ID to search
730
+ * sort {string} The sort column
731
+ * sortOrder {string} The sort order ("ascending" or "descending")
732
+ * sortType {string} Whether to treat sortable values as strings or
733
+ * numbers. ("string" or "number")
734
+ * limit {number} The desired number of results
735
+ * @returns {Promise<{rows: *}>}
736
+ */
737
+ export async function fullSearch<T>(
738
+ dbName: string,
739
+ index: string,
740
+ query: SearchFilters,
741
+ params: SearchParams<T>
742
+ ) {
743
+ let limit = params.limit
744
+ if (limit == null || isNaN(limit) || limit < 0) {
745
+ limit = 1000
746
+ }
747
+ params.limit = Math.min(limit, 1000)
748
+ const rows = await recursiveSearch<T>(dbName, index, query, params)
749
+ return { rows }
750
+ }