velocious 1.0.473 → 1.0.475
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/README.md +7 -2
- package/build/authorization/base-resource.js +99 -0
- package/build/configuration-types.js +41 -3
- package/build/configuration.js +41 -1
- package/build/database/drivers/base.js +51 -3
- package/build/database/drivers/mysql/index.js +74 -7
- package/build/database/drivers/sqlite/connection-sql-js.js +16 -5
- package/build/database/drivers/sqlite/index.web.js +29 -15
- package/build/database/drivers/sqlite/web-persistence.js +521 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
- package/build/frontend-model-controller.js +139 -16
- package/build/frontend-model-resource/base-resource.js +258 -46
- package/build/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/build/frontend-models/base.js +146 -2
- package/build/frontend-models/query.js +3 -3
- package/build/frontend-models/resource-definition.js +400 -23
- package/build/routes/hooks/frontend-model-command-route-hook.js +8 -0
- package/build/src/authorization/base-resource.d.ts +41 -0
- package/build/src/authorization/base-resource.d.ts.map +1 -1
- package/build/src/authorization/base-resource.js +88 -1
- package/build/src/configuration-types.d.ts +128 -5
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +38 -4
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +37 -2
- package/build/src/database/drivers/base.d.ts +30 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +47 -4
- package/build/src/database/drivers/mysql/index.d.ts +32 -3
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +65 -7
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +8 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.js +15 -6
- package/build/src/database/drivers/sqlite/index.web.d.ts +5 -5
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.js +27 -13
- package/build/src/database/drivers/sqlite/web-persistence.d.ts +77 -0
- package/build/src/database/drivers/sqlite/web-persistence.d.ts.map +1 -0
- package/build/src/database/drivers/sqlite/web-persistence.js +464 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +30 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +54 -2
- package/build/src/frontend-model-controller.d.ts +34 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +125 -17
- package/build/src/frontend-model-resource/base-resource.d.ts +69 -0
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +247 -54
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +0 -2
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +1 -3
- package/build/src/frontend-models/base.d.ts +29 -0
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +131 -3
- package/build/src/frontend-models/query.js +4 -4
- package/build/src/frontend-models/resource-definition.d.ts +6 -0
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +355 -28
- package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
- package/build/src/routes/hooks/frontend-model-command-route-hook.js +8 -1
- package/build/src/sync/device-identity.d.ts +240 -0
- package/build/src/sync/device-identity.d.ts.map +1 -0
- package/build/src/sync/device-identity.js +454 -0
- package/build/src/sync/local-mutation-log.d.ts +212 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -0
- package/build/src/sync/local-mutation-log.js +404 -0
- package/build/src/sync/offline-grant.d.ts +157 -0
- package/build/src/sync/offline-grant.d.ts.map +1 -0
- package/build/src/sync/offline-grant.js +292 -0
- package/build/sync/device-identity.js +503 -0
- package/build/sync/local-mutation-log.js +448 -0
- package/build/sync/offline-grant.js +327 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/scripts/tensorbuzz-retry +21 -0
- package/src/authorization/base-resource.js +99 -0
- package/src/configuration-types.js +41 -3
- package/src/configuration.js +41 -1
- package/src/database/drivers/base.js +51 -3
- package/src/database/drivers/mysql/index.js +74 -7
- package/src/database/drivers/sqlite/connection-sql-js.js +16 -5
- package/src/database/drivers/sqlite/index.web.js +29 -15
- package/src/database/drivers/sqlite/web-persistence.js +521 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
- package/src/frontend-model-controller.js +139 -16
- package/src/frontend-model-resource/base-resource.js +258 -46
- package/src/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/src/frontend-models/base.js +146 -2
- package/src/frontend-models/query.js +3 -3
- package/src/frontend-models/resource-definition.js +400 -23
- package/src/routes/hooks/frontend-model-command-route-hook.js +8 -0
- package/src/sync/device-identity.js +503 -0
- package/src/sync/local-mutation-log.js +448 -0
- 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
|
+
}
|
|
@@ -442,6 +442,13 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
442
442
|
}
|
|
443
443
|
fileContent += " },\n"
|
|
444
444
|
}
|
|
445
|
+
if (modelConfig.sync?.enabled) {
|
|
446
|
+
fileContent += this.formattedJsonProperty({
|
|
447
|
+
indent: " ",
|
|
448
|
+
propertyName: "sync",
|
|
449
|
+
value: modelConfig.sync
|
|
450
|
+
})
|
|
451
|
+
}
|
|
445
452
|
fileContent += " }\n"
|
|
446
453
|
fileContent += " }\n"
|
|
447
454
|
|
|
@@ -999,6 +1006,62 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
999
1006
|
return output
|
|
1000
1007
|
}
|
|
1001
1008
|
|
|
1009
|
+
/**
|
|
1010
|
+
* Runs formatted JSON property.
|
|
1011
|
+
* @param {object} args - Formatting args.
|
|
1012
|
+
* @param {string} args.indent - Base indentation.
|
|
1013
|
+
* @param {string} args.propertyName - Object property name.
|
|
1014
|
+
* @param {unknown} args.value - JSON-compatible value.
|
|
1015
|
+
* @returns {string} - Formatted property.
|
|
1016
|
+
*/
|
|
1017
|
+
formattedJsonProperty({indent, propertyName, value}) {
|
|
1018
|
+
return `${indent}${propertyName}: ${this.formattedJsonValue({indent, value})},\n`
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Runs formatted JSON value.
|
|
1023
|
+
* @param {object} args - Formatting args.
|
|
1024
|
+
* @param {string} args.indent - Indentation before this value.
|
|
1025
|
+
* @param {unknown} args.value - JSON-compatible value.
|
|
1026
|
+
* @returns {string} - Formatted value.
|
|
1027
|
+
*/
|
|
1028
|
+
formattedJsonValue({indent, value}) {
|
|
1029
|
+
if (Array.isArray(value)) {
|
|
1030
|
+
let output = "[\n"
|
|
1031
|
+
|
|
1032
|
+
for (const entry of value) {
|
|
1033
|
+
output += `${indent} ${this.formattedJsonValue({indent: `${indent} `, value: entry})},\n`
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
output += `${indent}]`
|
|
1037
|
+
|
|
1038
|
+
return output
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (value && typeof value === "object") {
|
|
1042
|
+
let output = "{\n"
|
|
1043
|
+
|
|
1044
|
+
for (const key of Object.keys(value)) {
|
|
1045
|
+
output += `${indent} ${this.formattedObjectKey(key)}: ${this.formattedJsonValue({indent: `${indent} `, value: /** @type {Record<string, unknown>} */ (value)[key]})},\n`
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
output += `${indent}}`
|
|
1049
|
+
|
|
1050
|
+
return output
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return JSON.stringify(value)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Runs formatted object key.
|
|
1058
|
+
* @param {string} key - Object key.
|
|
1059
|
+
* @returns {string} - JavaScript object key.
|
|
1060
|
+
*/
|
|
1061
|
+
formattedObjectKey(key) {
|
|
1062
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1002
1065
|
/**
|
|
1003
1066
|
* Runs attribute definitions for model.
|
|
1004
1067
|
* @param {object} args - Arguments.
|
|
@@ -1439,7 +1502,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
1439
1502
|
*/
|
|
1440
1503
|
frontendAttributeIsTranslated({attributeName, modelClass, resourceClass}) {
|
|
1441
1504
|
if (resourceClass) {
|
|
1442
|
-
const translatedAttributes = resourceClass.
|
|
1505
|
+
const translatedAttributes = resourceClass.translatedAttributesConfig()
|
|
1443
1506
|
|
|
1444
1507
|
if (Array.isArray(translatedAttributes) && translatedAttributes.includes(attributeName)) return true
|
|
1445
1508
|
}
|