velocious 1.0.474 → 1.0.476

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 (141) hide show
  1. package/README.md +10 -2
  2. package/build/configuration-types.js +39 -1
  3. package/build/configuration.js +46 -1
  4. package/build/database/drivers/base.js +67 -3
  5. package/build/database/drivers/mssql/index.js +13 -0
  6. package/build/database/drivers/mssql/sql/remove-index.js +6 -0
  7. package/build/database/drivers/mysql/index.js +87 -7
  8. package/build/database/drivers/mysql/sql/remove-index.js +6 -0
  9. package/build/database/drivers/pgsql/index.js +13 -0
  10. package/build/database/drivers/pgsql/sql/remove-index.js +6 -0
  11. package/build/database/drivers/sqlite/base.js +13 -0
  12. package/build/database/drivers/sqlite/connection-sql-js.js +16 -5
  13. package/build/database/drivers/sqlite/index.web.js +29 -15
  14. package/build/database/drivers/sqlite/sql/remove-index.js +6 -0
  15. package/build/database/drivers/sqlite/web-persistence.js +521 -0
  16. package/build/database/migration/index.js +44 -0
  17. package/build/database/pool/base-methods-forward.js +1 -0
  18. package/build/database/query/remove-index-base.js +39 -0
  19. package/build/database/record/index.js +25 -9
  20. package/build/environment-handlers/node/cli/commands/generate/base-models.js +36 -0
  21. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +63 -0
  22. package/build/frontend-model-controller.js +401 -4
  23. package/build/frontend-model-resource/base-resource.js +5 -1
  24. package/build/frontend-models/base.js +146 -2
  25. package/build/frontend-models/query.js +3 -3
  26. package/build/frontend-models/resource-definition.js +304 -2
  27. package/build/routes/hooks/frontend-model-command-route-hook.js +16 -0
  28. package/build/src/configuration-types.d.ts +118 -2
  29. package/build/src/configuration-types.d.ts.map +1 -1
  30. package/build/src/configuration-types.js +36 -2
  31. package/build/src/configuration.d.ts +18 -1
  32. package/build/src/configuration.d.ts.map +1 -1
  33. package/build/src/configuration.js +42 -2
  34. package/build/src/database/drivers/base.d.ts +50 -0
  35. package/build/src/database/drivers/base.d.ts.map +1 -1
  36. package/build/src/database/drivers/base.js +62 -4
  37. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  38. package/build/src/database/drivers/mssql/index.js +12 -1
  39. package/build/src/database/drivers/mssql/sql/remove-index.d.ts +4 -0
  40. package/build/src/database/drivers/mssql/sql/remove-index.d.ts.map +1 -0
  41. package/build/src/database/drivers/mssql/sql/remove-index.js +5 -0
  42. package/build/src/database/drivers/mysql/index.d.ts +32 -3
  43. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  44. package/build/src/database/drivers/mysql/index.js +76 -7
  45. package/build/src/database/drivers/mysql/sql/remove-index.d.ts +4 -0
  46. package/build/src/database/drivers/mysql/sql/remove-index.d.ts.map +1 -0
  47. package/build/src/database/drivers/mysql/sql/remove-index.js +5 -0
  48. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  49. package/build/src/database/drivers/pgsql/index.js +12 -1
  50. package/build/src/database/drivers/pgsql/sql/remove-index.d.ts +4 -0
  51. package/build/src/database/drivers/pgsql/sql/remove-index.d.ts.map +1 -0
  52. package/build/src/database/drivers/pgsql/sql/remove-index.js +5 -0
  53. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  54. package/build/src/database/drivers/sqlite/base.js +12 -1
  55. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +8 -1
  56. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  57. package/build/src/database/drivers/sqlite/connection-sql-js.js +15 -6
  58. package/build/src/database/drivers/sqlite/index.web.d.ts +5 -5
  59. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  60. package/build/src/database/drivers/sqlite/index.web.js +27 -13
  61. package/build/src/database/drivers/sqlite/sql/remove-index.d.ts +4 -0
  62. package/build/src/database/drivers/sqlite/sql/remove-index.d.ts.map +1 -0
  63. package/build/src/database/drivers/sqlite/sql/remove-index.js +5 -0
  64. package/build/src/database/drivers/sqlite/web-persistence.d.ts +77 -0
  65. package/build/src/database/drivers/sqlite/web-persistence.d.ts.map +1 -0
  66. package/build/src/database/drivers/sqlite/web-persistence.js +464 -0
  67. package/build/src/database/migration/index.d.ts +25 -0
  68. package/build/src/database/migration/index.d.ts.map +1 -1
  69. package/build/src/database/migration/index.js +39 -1
  70. package/build/src/database/pool/base-methods-forward.d.ts.map +1 -1
  71. package/build/src/database/pool/base-methods-forward.js +2 -1
  72. package/build/src/database/query/remove-index-base.d.ts +35 -0
  73. package/build/src/database/query/remove-index-base.d.ts.map +1 -0
  74. package/build/src/database/query/remove-index-base.js +34 -0
  75. package/build/src/database/record/index.d.ts +11 -0
  76. package/build/src/database/record/index.d.ts.map +1 -1
  77. package/build/src/database/record/index.js +26 -11
  78. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts +15 -0
  79. package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
  80. package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +33 -1
  81. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +30 -0
  82. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  83. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +53 -1
  84. package/build/src/frontend-model-controller.d.ts +96 -0
  85. package/build/src/frontend-model-controller.d.ts.map +1 -1
  86. package/build/src/frontend-model-controller.js +359 -5
  87. package/build/src/frontend-model-resource/base-resource.d.ts +4 -2
  88. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  89. package/build/src/frontend-model-resource/base-resource.js +7 -2
  90. package/build/src/frontend-models/base.d.ts +29 -0
  91. package/build/src/frontend-models/base.d.ts.map +1 -1
  92. package/build/src/frontend-models/base.js +131 -3
  93. package/build/src/frontend-models/query.js +4 -4
  94. package/build/src/frontend-models/resource-definition.d.ts +6 -0
  95. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  96. package/build/src/frontend-models/resource-definition.js +272 -3
  97. package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
  98. package/build/src/routes/hooks/frontend-model-command-route-hook.js +15 -1
  99. package/build/src/sync/device-identity.d.ts +240 -0
  100. package/build/src/sync/device-identity.d.ts.map +1 -0
  101. package/build/src/sync/device-identity.js +454 -0
  102. package/build/src/sync/local-mutation-log.d.ts +212 -0
  103. package/build/src/sync/local-mutation-log.d.ts.map +1 -0
  104. package/build/src/sync/local-mutation-log.js +404 -0
  105. package/build/src/sync/offline-grant.d.ts +157 -0
  106. package/build/src/sync/offline-grant.d.ts.map +1 -0
  107. package/build/src/sync/offline-grant.js +292 -0
  108. package/build/sync/device-identity.js +503 -0
  109. package/build/sync/local-mutation-log.js +448 -0
  110. package/build/sync/offline-grant.js +327 -0
  111. package/build/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +3 -3
  113. package/src/configuration-types.js +39 -1
  114. package/src/configuration.js +46 -1
  115. package/src/database/drivers/base.js +67 -3
  116. package/src/database/drivers/mssql/index.js +13 -0
  117. package/src/database/drivers/mssql/sql/remove-index.js +6 -0
  118. package/src/database/drivers/mysql/index.js +87 -7
  119. package/src/database/drivers/mysql/sql/remove-index.js +6 -0
  120. package/src/database/drivers/pgsql/index.js +13 -0
  121. package/src/database/drivers/pgsql/sql/remove-index.js +6 -0
  122. package/src/database/drivers/sqlite/base.js +13 -0
  123. package/src/database/drivers/sqlite/connection-sql-js.js +16 -5
  124. package/src/database/drivers/sqlite/index.web.js +29 -15
  125. package/src/database/drivers/sqlite/sql/remove-index.js +6 -0
  126. package/src/database/drivers/sqlite/web-persistence.js +521 -0
  127. package/src/database/migration/index.js +44 -0
  128. package/src/database/pool/base-methods-forward.js +1 -0
  129. package/src/database/query/remove-index-base.js +39 -0
  130. package/src/database/record/index.js +25 -9
  131. package/src/environment-handlers/node/cli/commands/generate/base-models.js +36 -0
  132. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +63 -0
  133. package/src/frontend-model-controller.js +401 -4
  134. package/src/frontend-model-resource/base-resource.js +5 -1
  135. package/src/frontend-models/base.js +146 -2
  136. package/src/frontend-models/query.js +3 -3
  137. package/src/frontend-models/resource-definition.js +304 -2
  138. package/src/routes/hooks/frontend-model-command-route-hook.js +16 -0
  139. package/src/sync/device-identity.js +503 -0
  140. package/src/sync/local-mutation-log.js +448 -0
  141. package/src/sync/offline-grant.js +327 -0
@@ -0,0 +1,521 @@
1
+ // @ts-check
2
+
3
+ import BetterLocalStorage from "better-localstorage"
4
+
5
+ const SUPPORT_CHECK_FILE = ".velocious-opfs-support-check"
6
+ const SUPPORT_CHECK_BYTES = new Uint8Array([118, 101, 108, 111, 99, 105, 111, 117, 115])
7
+
8
+ /**
9
+ * SQLite web persistence adapter.
10
+ * @typedef {object} SqliteWebPersistence
11
+ * @property {"indexeddb" | "localstorage" | "opfs"} name - Persistence backend name.
12
+ * @property {() => Promise<void>} delete - Deletes the persisted database.
13
+ * @property {() => Promise<Uint8Array | undefined>} load - Loads persisted database bytes.
14
+ * @property {(content: Uint8Array) => Promise<void>} save - Saves persisted database bytes.
15
+ */
16
+
17
+ /**
18
+ * Browser-like environment used for web persistence detection.
19
+ * @typedef {object} SqliteWebPersistenceEnvironment
20
+ * @property {unknown} [indexedDB] - IndexedDB global.
21
+ * @property {unknown} [navigator] - Navigator global.
22
+ */
23
+
24
+ /**
25
+ * Creates the best SQLite web persistence adapter supported by the current browser.
26
+ * @param {object} args - Arguments.
27
+ * @param {string} args.databaseName - Database name.
28
+ * @param {SqliteWebPersistenceEnvironment} [args.environment] - Browser-like environment.
29
+ * @returns {Promise<SqliteWebPersistence>} - Selected persistence adapter.
30
+ */
31
+ export async function createSqliteWebPersistence({databaseName, environment = globalThis}) {
32
+ const localStoragePersistence = new LocalStoragePersistence({databaseName})
33
+ const opfsPersistence = new OpfsPersistence({databaseName, environment})
34
+ const indexedDbPersistence = new IndexedDbPersistence({databaseName, environment})
35
+
36
+ const selectedPersistence = await selectSupportedPersistence({environment, indexedDbPersistence, localStoragePersistence, opfsPersistence})
37
+
38
+ await migratePersistedDatabase({
39
+ databaseName,
40
+ destinationPersistence: selectedPersistence,
41
+ environment,
42
+ sourcePersistences: [localStoragePersistence, indexedDbPersistence, opfsPersistence]
43
+ })
44
+
45
+ return selectedPersistence
46
+ }
47
+
48
+ /**
49
+ * Deletes SQLite web database bytes from every available persistence backend.
50
+ * @param {object} args - Arguments.
51
+ * @param {string} args.databaseName - Database name.
52
+ * @param {SqliteWebPersistenceEnvironment} [args.environment] - Browser-like environment.
53
+ * @returns {Promise<void>} - Resolves when all available backends were cleared.
54
+ */
55
+ export async function deleteSqliteWebPersistences({databaseName, environment = globalThis}) {
56
+ const persistences = [
57
+ new LocalStoragePersistence({databaseName}),
58
+ new OpfsPersistence({databaseName, environment}),
59
+ new IndexedDbPersistence({databaseName, environment})
60
+ ]
61
+
62
+ for (const persistence of persistences) await deletePersistenceIfAvailable(persistence)
63
+ }
64
+
65
+ /**
66
+ * Returns the legacy SQLite web storage key for a database name.
67
+ * @param {string} databaseName - Database name.
68
+ * @returns {string} - Persistence key.
69
+ */
70
+ export function sqliteWebPersistenceKey(databaseName) {
71
+ if (!databaseName) throw new Error("No name given in arguments for SQLite Web database")
72
+
73
+ return `VelociousDatabaseDriversSqliteWeb---${databaseName}`
74
+ }
75
+
76
+ /** OPFS-backed SQL.js database file persistence. */
77
+ class OpfsPersistence {
78
+ /** @type {"opfs"} */
79
+ name = "opfs"
80
+
81
+ /**
82
+ * Creates OPFS persistence.
83
+ * @param {object} args - Arguments.
84
+ * @param {string} args.databaseName - Database name.
85
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
86
+ */
87
+ constructor({databaseName, environment}) {
88
+ this.databaseName = databaseName
89
+ this.environment = environment
90
+ }
91
+
92
+ /**
93
+ * Deletes the OPFS database file.
94
+ * @returns {Promise<void>} - Resolves when deleted.
95
+ */
96
+ async delete() {
97
+ const directory = await opfsDirectory(this.environment)
98
+
99
+ try {
100
+ await directory.removeEntry(sqliteWebPersistenceKey(this.databaseName))
101
+ } catch (error) {
102
+ if (!isNotFoundError(error)) throw error
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Loads the OPFS database file.
108
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes.
109
+ */
110
+ async load() {
111
+ const directory = await opfsDirectory(this.environment)
112
+
113
+ try {
114
+ const fileHandle = await directory.getFileHandle(sqliteWebPersistenceKey(this.databaseName))
115
+ const file = await fileHandle.getFile()
116
+ const arrayBuffer = await file.arrayBuffer()
117
+
118
+ return new Uint8Array(arrayBuffer)
119
+ } catch (error) {
120
+ if (isNotFoundError(error)) return undefined
121
+
122
+ throw error
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Checks whether the OPFS database file exists.
128
+ * @returns {Promise<boolean>} - Whether content exists.
129
+ */
130
+ async exists() {
131
+ try {
132
+ return (await this.load()) !== undefined
133
+ } catch {
134
+ return false
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Saves database bytes.
140
+ * @param {Uint8Array} content - Database bytes.
141
+ * @returns {Promise<void>} - Resolves when saved.
142
+ */
143
+ async save(content) {
144
+ const directory = await opfsDirectory(this.environment)
145
+ const fileHandle = await directory.getFileHandle(sqliteWebPersistenceKey(this.databaseName), {create: true})
146
+ const writable = await fileHandle.createWritable()
147
+
148
+ await writable.write(arrayBufferFromBytes(content))
149
+ await writable.close()
150
+ }
151
+ }
152
+
153
+ /** IndexedDB-backed SQL.js database blob persistence. */
154
+ class IndexedDbPersistence {
155
+ /** @type {"indexeddb"} */
156
+ name = "indexeddb"
157
+
158
+ /**
159
+ * Creates IndexedDB persistence.
160
+ * @param {object} args - Arguments.
161
+ * @param {string} args.databaseName - Database name.
162
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
163
+ */
164
+ constructor({databaseName, environment}) {
165
+ this.databaseName = databaseName
166
+ this.environment = environment
167
+ }
168
+
169
+ /**
170
+ * Deletes the IndexedDB database entry.
171
+ * @returns {Promise<void>} - Resolves when deleted.
172
+ */
173
+ async delete() {
174
+ const database = await openIndexedDb(this.environment)
175
+
176
+ await indexedDbRequest(database.transaction("databases", "readwrite").objectStore("databases").delete(sqliteWebPersistenceKey(this.databaseName)))
177
+ database.close()
178
+ }
179
+
180
+ /**
181
+ * Loads the IndexedDB database entry.
182
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes.
183
+ */
184
+ async load() {
185
+ const database = await openIndexedDb(this.environment)
186
+ const result = await indexedDbRequest(database.transaction("databases", "readonly").objectStore("databases").get(sqliteWebPersistenceKey(this.databaseName)))
187
+
188
+ database.close()
189
+
190
+ if (result === undefined) return undefined
191
+ if (result instanceof Uint8Array) return result
192
+ if (result instanceof ArrayBuffer) return new Uint8Array(result)
193
+
194
+ throw new Error("SQLite web IndexedDB persistence returned unsupported content")
195
+ }
196
+
197
+ /**
198
+ * Checks whether the IndexedDB database entry exists.
199
+ * @returns {Promise<boolean>} - Whether content exists.
200
+ */
201
+ async exists() {
202
+ try {
203
+ const database = await openIndexedDb(this.environment)
204
+ const result = await indexedDbRequest(database.transaction("databases", "readonly").objectStore("databases").get(sqliteWebPersistenceKey(this.databaseName)))
205
+
206
+ database.close()
207
+
208
+ return result !== undefined && result !== null
209
+ } catch {
210
+ return false
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Saves database bytes.
216
+ * @param {Uint8Array} content - Database bytes.
217
+ * @returns {Promise<void>} - Resolves when saved.
218
+ */
219
+ async save(content) {
220
+ const database = await openIndexedDb(this.environment)
221
+
222
+ await indexedDbRequest(database.transaction("databases", "readwrite").objectStore("databases").put(content, sqliteWebPersistenceKey(this.databaseName)))
223
+ database.close()
224
+ }
225
+ }
226
+
227
+ /** LocalStorage-backed SQL.js database blob persistence for legacy migrations. */
228
+ class LocalStoragePersistence {
229
+
230
+ /** @type {"localstorage"} */
231
+ name = "localstorage"
232
+
233
+ /**
234
+ * Creates localStorage persistence.
235
+ * @param {object} args - Arguments.
236
+ * @param {string} args.databaseName - Database name.
237
+ */
238
+ constructor({databaseName}) {
239
+ this.databaseName = databaseName
240
+ /** @type {BetterLocalStorage | undefined} */
241
+ this.storage = undefined
242
+ }
243
+
244
+ /**
245
+ * Deletes the localStorage database entry.
246
+ * @returns {Promise<void>} - Resolves when deleted.
247
+ */
248
+ async delete() {
249
+ await this.localStorage().delete(sqliteWebPersistenceKey(this.databaseName))
250
+ }
251
+
252
+ /**
253
+ * Loads the localStorage database entry.
254
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes.
255
+ */
256
+ async load() {
257
+ const content = await this.localStorage().get(sqliteWebPersistenceKey(this.databaseName))
258
+
259
+ if (content === null || content === undefined) return undefined
260
+ if (content instanceof Uint8Array) return content
261
+ if (content instanceof ArrayBuffer) return new Uint8Array(content)
262
+
263
+ return /** @type {Uint8Array} */ (content)
264
+ }
265
+
266
+ /**
267
+ * Saves database bytes.
268
+ * @param {Uint8Array} content - Database bytes.
269
+ * @returns {Promise<void>} - Resolves when saved.
270
+ */
271
+ async save(content) {
272
+ await this.localStorage().set(sqliteWebPersistenceKey(this.databaseName), content)
273
+ }
274
+
275
+ /**
276
+ * Checks whether the legacy localStorage database exists.
277
+ * @returns {Promise<boolean>} - Whether content exists.
278
+ */
279
+ async exists() {
280
+ try {
281
+ const content = await this.localStorage().get(sqliteWebPersistenceKey(this.databaseName))
282
+
283
+ return content !== undefined && content !== null
284
+ } catch {
285
+ return false
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Returns the localStorage wrapper.
291
+ * @returns {BetterLocalStorage} - Storage wrapper.
292
+ */
293
+ localStorage() {
294
+ this.storage ||= new BetterLocalStorage()
295
+
296
+ return this.storage
297
+ }
298
+ }
299
+
300
+
301
+ /**
302
+ * Selects the preferred available SQLite web persistence backend.
303
+ * @param {object} args - Arguments.
304
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
305
+ * @param {IndexedDbPersistence} args.indexedDbPersistence - IndexedDB persistence adapter.
306
+ * @param {LocalStoragePersistence} args.localStoragePersistence - Legacy localStorage persistence adapter.
307
+ * @param {OpfsPersistence} args.opfsPersistence - OPFS persistence adapter.
308
+ * @returns {Promise<SqliteWebPersistence>} - Selected persistence adapter.
309
+ */
310
+ async function selectSupportedPersistence({environment, indexedDbPersistence, localStoragePersistence, opfsPersistence}) {
311
+ if (await supportsOpfsPersistence(environment)) return opfsPersistence
312
+ if (await supportsIndexedDbPersistence(environment)) return indexedDbPersistence
313
+
314
+ return localStoragePersistence
315
+ }
316
+
317
+ /**
318
+ * Migrates any existing database bytes into the selected persistence backend.
319
+ * @param {object} args - Arguments.
320
+ * @param {string} args.databaseName - Database name.
321
+ * @param {SqliteWebPersistence} args.destinationPersistence - Selected persistence adapter.
322
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
323
+ * @param {{delete: () => Promise<void>, load: () => Promise<Uint8Array | undefined>}[]} args.sourcePersistences - Persistence adapters to scan for existing bytes.
324
+ * @returns {Promise<void>} - Resolves when migration is complete.
325
+ */
326
+ async function migratePersistedDatabase({databaseName, destinationPersistence, environment, sourcePersistences}) {
327
+ if (await destinationPersistence.load() !== undefined) return
328
+
329
+ for (const sourcePersistence of sourcePersistences) {
330
+ if (sourcePersistence === destinationPersistence) continue
331
+
332
+ const databaseBytes = await loadPersistenceIfAvailable(sourcePersistence)
333
+ if (databaseBytes === undefined) continue
334
+
335
+ await destinationPersistence.save(databaseBytes)
336
+ await deleteSqliteWebPersistences({databaseName, environment})
337
+ await destinationPersistence.save(databaseBytes)
338
+ return
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Loads a persistence backend, ignoring unavailable backend errors.
344
+ * @param {{load: () => Promise<Uint8Array | undefined>}} persistence - Persistence adapter.
345
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes, if available.
346
+ */
347
+ async function loadPersistenceIfAvailable(persistence) {
348
+ try {
349
+ return await persistence.load()
350
+ } catch {
351
+ return undefined
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Tests whether OPFS persistence is usable.
357
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
358
+ * @returns {Promise<boolean>} - Whether OPFS can be used.
359
+ */
360
+ async function supportsOpfsPersistence(environment) {
361
+ try {
362
+ const directory = await opfsDirectory(environment)
363
+ const fileHandle = await directory.getFileHandle(SUPPORT_CHECK_FILE, {create: true})
364
+ const writable = await fileHandle.createWritable()
365
+
366
+ await writable.write(arrayBufferFromBytes(SUPPORT_CHECK_BYTES))
367
+ await writable.close()
368
+
369
+ const file = await fileHandle.getFile()
370
+ const readBack = new Uint8Array(await file.arrayBuffer())
371
+
372
+ await directory.removeEntry(SUPPORT_CHECK_FILE)
373
+
374
+ return sameBytes(readBack, SUPPORT_CHECK_BYTES)
375
+ } catch {
376
+ return false
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Tests whether IndexedDB persistence is usable.
382
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
383
+ * @returns {Promise<boolean>} - Whether IndexedDB can be used.
384
+ */
385
+ async function supportsIndexedDbPersistence(environment) {
386
+ try {
387
+ const database = await openIndexedDb(environment)
388
+ const store = database.transaction("databases", "readwrite").objectStore("databases")
389
+
390
+ await indexedDbRequest(store.put(SUPPORT_CHECK_BYTES, SUPPORT_CHECK_FILE))
391
+
392
+ const readBack = await indexedDbRequest(store.get(SUPPORT_CHECK_FILE))
393
+
394
+ await indexedDbRequest(store.delete(SUPPORT_CHECK_FILE))
395
+ database.close()
396
+
397
+ return readBack instanceof Uint8Array && sameBytes(readBack, SUPPORT_CHECK_BYTES)
398
+ } catch {
399
+ return false
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Opens the SQLite web IndexedDB database.
405
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
406
+ * @returns {Promise<IDBDatabase>} - Open database.
407
+ */
408
+ async function openIndexedDb(environment) {
409
+ const indexedDb = indexedDbFromEnvironment(environment)
410
+
411
+ if (!indexedDb || typeof indexedDb.open !== "function") throw new Error("IndexedDB is not available")
412
+
413
+ const request = indexedDb.open("VelociousDatabaseDriversSqliteWeb", 1)
414
+ request.onupgradeneeded = () => {
415
+ const database = request.result
416
+
417
+ if (!database.objectStoreNames.contains("databases")) database.createObjectStore("databases")
418
+ }
419
+
420
+ return await indexedDbRequest(request)
421
+ }
422
+
423
+ /**
424
+ * Resolves an IndexedDB request.
425
+ * @template T
426
+ * @param {IDBRequest<T>} request - IndexedDB request.
427
+ * @returns {Promise<T>} - Request result.
428
+ */
429
+ function indexedDbRequest(request) {
430
+ return new Promise((resolve, reject) => {
431
+ request.onsuccess = () => resolve(request.result)
432
+ request.onerror = () => reject(request.error || new Error("IndexedDB request failed"))
433
+ })
434
+ }
435
+
436
+ /**
437
+ * Deletes a persistence backend, ignoring unavailable backend errors.
438
+ * @param {{delete: () => Promise<void>}} persistence - Persistence adapter.
439
+ * @returns {Promise<void>} - Resolves when deletion was attempted.
440
+ */
441
+ async function deletePersistenceIfAvailable(persistence) {
442
+ try {
443
+ await persistence.delete()
444
+ } catch {
445
+ // Ignore unavailable backends so reset clears every backend the browser can access.
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Gets OPFS root directory.
451
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
452
+ * @returns {Promise<FileSystemDirectoryHandle>} - OPFS root directory.
453
+ */
454
+ async function opfsDirectory(environment) {
455
+ const navigatorObject = navigatorFromEnvironment(environment)
456
+ const storage = navigatorObject.storage
457
+
458
+ if (!storage || typeof storage.getDirectory !== "function") throw new Error("OPFS is not available")
459
+
460
+ return await storage.getDirectory()
461
+ }
462
+
463
+ /**
464
+ * Gets navigator from environment.
465
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
466
+ * @returns {{storage?: {getDirectory?: () => Promise<FileSystemDirectoryHandle>}}} - Navigator-like object.
467
+ */
468
+ function navigatorFromEnvironment(environment) {
469
+ const candidate = environment.navigator
470
+
471
+ if (!candidate || typeof candidate !== "object") return {}
472
+
473
+ return /** @type {{storage?: {getDirectory?: () => Promise<FileSystemDirectoryHandle>}}} */ (candidate)
474
+ }
475
+
476
+ /**
477
+ * Gets IndexedDB from environment.
478
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
479
+ * @returns {{open?: (name: string, version?: number) => IDBOpenDBRequest} | undefined} - IndexedDB-like object.
480
+ */
481
+ function indexedDbFromEnvironment(environment) {
482
+ const candidate = environment.indexedDB
483
+
484
+ if (!candidate || typeof candidate !== "object") return undefined
485
+
486
+ return /** @type {{open?: (name: string, version?: number) => IDBOpenDBRequest}} */ (candidate)
487
+ }
488
+
489
+ /**
490
+ * Converts bytes to a standalone ArrayBuffer for browser file writes.
491
+ * @param {Uint8Array} bytes - Bytes to convert.
492
+ * @returns {ArrayBuffer} - Standalone ArrayBuffer.
493
+ */
494
+ function arrayBufferFromBytes(bytes) {
495
+ const copy = new Uint8Array(bytes.byteLength)
496
+
497
+ copy.set(bytes)
498
+
499
+ return copy.buffer
500
+ }
501
+
502
+ /**
503
+ * Checks whether an error is a file-not-found error.
504
+ * @param {unknown} error - Error candidate.
505
+ * @returns {boolean} - Whether the error is not found.
506
+ */
507
+ function isNotFoundError(error) {
508
+ return error instanceof Error && error.name === "NotFoundError"
509
+ }
510
+
511
+ /**
512
+ * Compares two byte arrays.
513
+ * @param {Uint8Array} left - Left bytes.
514
+ * @param {Uint8Array} right - Right bytes.
515
+ * @returns {boolean} - Whether bytes match.
516
+ */
517
+ function sameBytes(left, right) {
518
+ if (left.length !== right.length) return false
519
+
520
+ return left.every((value, index) => value === right[index])
521
+ }
@@ -39,6 +39,7 @@
39
39
  import { convertLegacyDateValueToUtcStorage } from "../datetime-storage.js"
40
40
  import * as inflection from "inflection"
41
41
  import restArgsError from "../../utils/rest-args-error.js"
42
+ import CreateIndexBase from "../query/create-index-base.js"
42
43
  import TableData from "../table-data/index.js"
43
44
  class NotImplementedError extends Error {}
44
45
 
@@ -184,6 +185,49 @@ export default class VelociousDatabaseMigration {
184
185
  }
185
186
  }
186
187
 
188
+ /**
189
+ * RemoveIndexArgsType type.
190
+ * @typedef {object} RemoveIndexArgsType
191
+ * @property {string} [name] - Explicit index name to remove.
192
+ */
193
+ /**
194
+ * Runs remove index.
195
+ * @param {string} tableName - Table name.
196
+ * @param {string | Array<string | import("../table-data/table-column.js").default>} nameOrColumns - Index name or columns whose default addIndex name should be removed.
197
+ * @param {RemoveIndexArgsType} [args] - Options object.
198
+ * @returns {Promise<void>} - Resolves when complete.
199
+ */
200
+ async removeIndex(tableName, nameOrColumns, args = {}) {
201
+ const {name, ...restArgs} = args
202
+
203
+ restArgsError(restArgs)
204
+
205
+ const removeIndexName = name || this._removeIndexName(tableName, nameOrColumns)
206
+ const sqls = await this.getDriver().removeIndexSQLs({name: removeIndexName, tableName})
207
+
208
+ for (const sql of sqls) {
209
+ await this.getDriver().query(sql)
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Runs remove index name.
215
+ * @param {string} tableName - Table name.
216
+ * @param {string | Array<string | import("../table-data/table-column.js").default>} nameOrColumns - Index name or columns.
217
+ * @returns {string} - The index name.
218
+ */
219
+ _removeIndexName(tableName, nameOrColumns) {
220
+ if (typeof nameOrColumns === "string") return nameOrColumns
221
+
222
+ const createIndex = new CreateIndexBase({
223
+ columns: nameOrColumns,
224
+ driver: this.getDriver(),
225
+ tableName
226
+ })
227
+
228
+ return createIndex.generateIndexName()
229
+ }
230
+
187
231
  /**
188
232
  * AddForeignKeyArgsType type.
189
233
  * @typedef {object} AddForeignKeyArgsType
@@ -23,6 +23,7 @@ export default function baseMethodsForward(PoolBase) {
23
23
  "quote",
24
24
  "quoteColumn",
25
25
  "quoteTable",
26
+ "removeIndexSQLs",
26
27
  "select",
27
28
  "update",
28
29
  "updateSql"
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+
3
+ import QueryBase from "./base.js"
4
+
5
+ /**
6
+ * RemoveIndexBaseArgsType type.
7
+ * @typedef {object} RemoveIndexBaseArgsType
8
+ * @property {import("../drivers/base.js").default} driver - Database driver used to generate SQL.
9
+ * @property {string} name - Index name to drop.
10
+ * @property {string} tableName - Name of the table the index belongs to.
11
+ */
12
+
13
+ export default class VelociousDatabaseQueryRemoveIndexBase extends QueryBase {
14
+ /**
15
+ * Runs constructor.
16
+ * @param {RemoveIndexBaseArgsType} args - Options object.
17
+ */
18
+ constructor({driver, name, tableName}) {
19
+ super({driver})
20
+ this.name = name
21
+ this.tableName = tableName
22
+ }
23
+
24
+ /**
25
+ * Runs to sqls.
26
+ * @returns {Promise<string[]>} - Resolves with SQL statements.
27
+ */
28
+ async toSQLs() {
29
+ const databaseType = this.getDriver().getType()
30
+ const options = this.getOptions()
31
+ let sql = `DROP INDEX ${options.quoteIndexName(this.name)}`
32
+
33
+ if (databaseType == "mssql" || databaseType == "mysql") {
34
+ sql += ` ON ${options.quoteTableName(this.tableName)}`
35
+ }
36
+
37
+ return [sql]
38
+ }
39
+ }
@@ -690,11 +690,14 @@ class VelociousDatabaseRecord {
690
690
  return await this.relationshipOrLoad(relationshipName, {preloadTranslations: true})
691
691
  }
692
692
 
693
- prototype[`set${inflection.camelize(relationshipName)}`] = function(/** @type {?} */ model) {
693
+ prototype[`set${inflection.camelize(relationshipName)}`] = function(/** @type {VelociousDatabaseRecord | null | undefined} */ model) {
694
694
  const relationship = this.getRelationshipByName(relationshipName)
695
+ const foreignKeyValue = this._belongsToForeignKeyValue({model, relationship})
695
696
 
696
- relationship.setLoaded(model)
697
+ relationship.setLoaded(model || undefined)
698
+ relationship.setPreloaded(true)
697
699
  relationship.setDirty(true)
700
+ this._setColumnAttribute(relationship.getForeignKey(), foreignKeyValue)
698
701
  }
699
702
  } else if (actualData.type == "hasMany") {
700
703
  relationship = new HasManyRelationship(actualData)
@@ -1783,6 +1786,20 @@ class VelociousDatabaseRecord {
1783
1786
  return loaded.readColumn(relationship.getPrimaryKey()) == normalizedValue
1784
1787
  }
1785
1788
 
1789
+ /**
1790
+ * Returns the foreign key value for a belongs-to relationship assignment.
1791
+ * @param {object} args - Relationship assignment arguments.
1792
+ * @param {VelociousDatabaseRecord | null | undefined} args.model - Assigned model.
1793
+ * @param {import("./instance-relationships/base.js").default} args.relationship - Belongs-to relationship instance.
1794
+ * @returns {string | number | null | undefined} - Foreign key value for the assignment.
1795
+ */
1796
+ _belongsToForeignKeyValue({model, relationship}) {
1797
+ if (model == null) return null
1798
+ if (!(model instanceof VelociousDatabaseRecord)) throw new Error(`Unexpected model type: ${typeof model}`)
1799
+
1800
+ return /** @type {string | number | null | undefined} */ (model.readColumn(relationship.getPrimaryKey()))
1801
+ }
1802
+
1786
1803
  /**
1787
1804
  * Runs clear loaded belongs to relationship.
1788
1805
  * @param {?} relationship - Relationship instance.
@@ -2364,8 +2381,9 @@ class VelociousDatabaseRecord {
2364
2381
  await model.save()
2365
2382
 
2366
2383
  const foreignKey = this._relationshipForeignKeyAttribute(instanceRelationship)
2384
+ const foreignKeyValue = this._belongsToForeignKeyValue({model, relationship: instanceRelationship})
2367
2385
 
2368
- this.setAttribute(foreignKey, model.id())
2386
+ this.setAttribute(foreignKey, foreignKeyValue)
2369
2387
 
2370
2388
  instanceRelationship.setPreloaded(true)
2371
2389
  instanceRelationship.setDirty(false)
@@ -3801,14 +3819,12 @@ class VelociousDatabaseRecord {
3801
3819
  const relationship = this._instanceRelationships[relationshipName]
3802
3820
 
3803
3821
  if (relationship.getType() == "belongsTo" && relationship.getDirty()) {
3804
- const model = relationship.loaded()
3822
+ const model = relationship.getLoadedOrUndefined()
3805
3823
 
3806
3824
  if (model) {
3807
- if (model instanceof VelociousDatabaseRecord) {
3808
- belongsToChanges[relationship.getForeignKey()] = model?.id()
3809
- } else {
3810
- throw new Error(`Unexpected model type: ${typeof model}`)
3811
- }
3825
+ if (Array.isArray(model)) throw new Error("Unexpected belongs-to model array")
3826
+
3827
+ belongsToChanges[relationship.getForeignKey()] = this._belongsToForeignKeyValue({model, relationship})
3812
3828
  }
3813
3829
  }
3814
3830
  }