@delma/fylo 2.1.0 → 2.1.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.
- package/README.md +27 -0
- package/dist/adapters/cipher.js +155 -0
- package/dist/adapters/cipher.js.map +1 -0
- package/dist/core/collection.js +6 -0
- package/dist/core/collection.js.map +1 -0
- package/{src/core/directory.ts → dist/core/directory.js} +28 -35
- package/dist/core/directory.js.map +1 -0
- package/dist/core/doc-id.js +15 -0
- package/dist/core/doc-id.js.map +1 -0
- package/dist/core/extensions.js +16 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/format.js +355 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/parser.js +764 -0
- package/dist/core/parser.js.map +1 -0
- package/dist/core/query.js +47 -0
- package/dist/core/query.js.map +1 -0
- package/dist/engines/s3-files/documents.js +62 -0
- package/dist/engines/s3-files/documents.js.map +1 -0
- package/dist/engines/s3-files/filesystem.js +165 -0
- package/dist/engines/s3-files/filesystem.js.map +1 -0
- package/dist/engines/s3-files/query.js +235 -0
- package/dist/engines/s3-files/query.js.map +1 -0
- package/dist/engines/s3-files/types.js +2 -0
- package/dist/engines/s3-files/types.js.map +1 -0
- package/dist/engines/s3-files.js +629 -0
- package/dist/engines/s3-files.js.map +1 -0
- package/dist/engines/types.js +2 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/index.js +562 -0
- package/dist/index.js.map +1 -0
- package/dist/sync.js +18 -0
- package/dist/sync.js.map +1 -0
- package/{src → dist}/types/fylo.d.ts +14 -1
- package/package.json +2 -2
- package/.env.example +0 -16
- package/.github/copilot-instructions.md +0 -3
- package/.github/prompts/release.prompt.md +0 -10
- package/.github/workflows/ci.yml +0 -37
- package/.github/workflows/publish.yml +0 -91
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -3
- package/CLAUDE.md +0 -3
- package/eslint.config.js +0 -32
- package/src/CLI +0 -39
- package/src/adapters/cipher.ts +0 -180
- package/src/core/collection.ts +0 -5
- package/src/core/extensions.ts +0 -21
- package/src/core/format.ts +0 -457
- package/src/core/parser.ts +0 -901
- package/src/core/query.ts +0 -53
- package/src/engines/s3-files/documents.ts +0 -65
- package/src/engines/s3-files/filesystem.ts +0 -172
- package/src/engines/s3-files/query.ts +0 -291
- package/src/engines/s3-files/types.ts +0 -42
- package/src/engines/s3-files.ts +0 -769
- package/src/engines/types.ts +0 -21
- package/src/index.ts +0 -632
- package/src/sync.ts +0 -58
- package/tests/collection/truncate.test.js +0 -36
- package/tests/data.js +0 -97
- package/tests/helpers/root.js +0 -7
- package/tests/integration/aws-s3-files.canary.test.js +0 -22
- package/tests/integration/create.test.js +0 -39
- package/tests/integration/delete.test.js +0 -97
- package/tests/integration/edge-cases.test.js +0 -162
- package/tests/integration/encryption.test.js +0 -148
- package/tests/integration/export.test.js +0 -46
- package/tests/integration/join-modes.test.js +0 -154
- package/tests/integration/nested.test.js +0 -144
- package/tests/integration/operators.test.js +0 -136
- package/tests/integration/read.test.js +0 -123
- package/tests/integration/rollback.test.js +0 -30
- package/tests/integration/s3-files.performance.test.js +0 -75
- package/tests/integration/s3-files.test.js +0 -205
- package/tests/integration/sync.test.js +0 -154
- package/tests/integration/update.test.js +0 -105
- package/tests/mocks/cipher.js +0 -40
- package/tests/schemas/album.d.ts +0 -5
- package/tests/schemas/album.json +0 -5
- package/tests/schemas/comment.d.ts +0 -7
- package/tests/schemas/comment.json +0 -7
- package/tests/schemas/photo.d.ts +0 -7
- package/tests/schemas/photo.json +0 -7
- package/tests/schemas/post.d.ts +0 -6
- package/tests/schemas/post.json +0 -6
- package/tests/schemas/tip.d.ts +0 -7
- package/tests/schemas/tip.json +0 -7
- package/tests/schemas/todo.d.ts +0 -6
- package/tests/schemas/todo.json +0 -6
- package/tests/schemas/user.d.ts +0 -23
- package/tests/schemas/user.json +0 -23
- package/tsconfig.json +0 -21
- package/tsconfig.typecheck.json +0 -31
- /package/{src → dist}/types/bun-runtime.d.ts +0 -0
- /package/{src → dist}/types/index.d.ts +0 -0
- /package/{src → dist}/types/node-runtime.d.ts +0 -0
- /package/{src → dist}/types/query.d.ts +0 -0
- /package/{src → dist}/types/vendor-modules.d.ts +0 -0
package/src/engines/types.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export type FyloStorageEngineKind = 's3-files'
|
|
2
|
-
|
|
3
|
-
export interface StorageEngine {
|
|
4
|
-
read(path: string): Promise<string>
|
|
5
|
-
write(path: string, data: string): Promise<void>
|
|
6
|
-
delete(path: string): Promise<void>
|
|
7
|
-
list(path: string): Promise<string[]>
|
|
8
|
-
mkdir(path: string): Promise<void>
|
|
9
|
-
rmdir(path: string): Promise<void>
|
|
10
|
-
exists(path: string): Promise<boolean>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface LockManager {
|
|
14
|
-
acquire(collection: string, docId: _ttid, owner: string, ttlMs?: number): Promise<boolean>
|
|
15
|
-
release(collection: string, docId: _ttid, owner: string): Promise<void>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface EventBus<T> {
|
|
19
|
-
publish(collection: string, event: T): Promise<void>
|
|
20
|
-
listen(collection: string): AsyncGenerator<T, void, unknown>
|
|
21
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,632 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { Parser } from './core/parser'
|
|
4
|
-
import TTID from '@delma/ttid'
|
|
5
|
-
import Gen from '@delma/chex'
|
|
6
|
-
import { Cipher } from './adapters/cipher'
|
|
7
|
-
import { S3FilesEngine } from './engines/s3-files'
|
|
8
|
-
import type { FyloOptions } from './sync'
|
|
9
|
-
import './core/format'
|
|
10
|
-
import './core/extensions'
|
|
11
|
-
|
|
12
|
-
export { FyloSyncError } from './sync'
|
|
13
|
-
export type {
|
|
14
|
-
FyloDeleteSyncEvent,
|
|
15
|
-
FyloOptions,
|
|
16
|
-
FyloSyncHooks,
|
|
17
|
-
FyloSyncMode,
|
|
18
|
-
FyloWriteSyncEvent
|
|
19
|
-
} from './sync'
|
|
20
|
-
|
|
21
|
-
export default class Fylo {
|
|
22
|
-
private static LOGGING = process.env.LOGGING
|
|
23
|
-
|
|
24
|
-
private static MAX_CPUS = navigator.hardwareConcurrency
|
|
25
|
-
|
|
26
|
-
private static readonly STRICT = process.env.STRICT
|
|
27
|
-
|
|
28
|
-
private static ttidLock: Promise<void> = Promise.resolve()
|
|
29
|
-
|
|
30
|
-
private static readonly SCHEMA_DIR = process.env.SCHEMA_DIR
|
|
31
|
-
|
|
32
|
-
/** Collections whose schema `$encrypted` config has already been loaded. */
|
|
33
|
-
private static readonly loadedEncryption: Set<string> = new Set()
|
|
34
|
-
|
|
35
|
-
private readonly engine: S3FilesEngine
|
|
36
|
-
|
|
37
|
-
constructor(options: FyloOptions = {}) {
|
|
38
|
-
this.engine = new S3FilesEngine(options.root ?? options.s3FilesRoot ?? Fylo.defaultRoot(), {
|
|
39
|
-
sync: options.sync,
|
|
40
|
-
syncMode: options.syncMode
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private static defaultRoot() {
|
|
45
|
-
return (
|
|
46
|
-
process.env.FYLO_ROOT ??
|
|
47
|
-
process.env.FYLO_S3FILES_ROOT ??
|
|
48
|
-
path.join(process.cwd(), '.fylo-data')
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private static get defaultEngine() {
|
|
53
|
-
return new S3FilesEngine(Fylo.defaultRoot())
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Executes a SQL query and returns the results.
|
|
58
|
-
* @param SQL The SQL query to execute.
|
|
59
|
-
* @returns The results of the query.
|
|
60
|
-
*/
|
|
61
|
-
async executeSQL<
|
|
62
|
-
T extends Record<string, any>,
|
|
63
|
-
U extends Record<string, any> = Record<string, unknown>
|
|
64
|
-
>(SQL: string) {
|
|
65
|
-
const op = SQL.match(/^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP)/i)
|
|
66
|
-
|
|
67
|
-
if (!op) throw new Error('Missing SQL Operation')
|
|
68
|
-
|
|
69
|
-
switch (op.shift()) {
|
|
70
|
-
case 'CREATE':
|
|
71
|
-
return await this.createCollection(
|
|
72
|
-
(Parser.parse(SQL) as _storeDelete<T>).$collection!
|
|
73
|
-
)
|
|
74
|
-
case 'DROP':
|
|
75
|
-
return await this.dropCollection(
|
|
76
|
-
(Parser.parse(SQL) as _storeDelete<T>).$collection!
|
|
77
|
-
)
|
|
78
|
-
case 'SELECT': {
|
|
79
|
-
const query = Parser.parse<T>(SQL) as _storeQuery<T>
|
|
80
|
-
if (SQL.includes('JOIN')) return await this.joinDocs(query as _join<T, U>)
|
|
81
|
-
const selCol = query.$collection
|
|
82
|
-
delete query.$collection
|
|
83
|
-
let docs: Record<string, unknown> | Array<_ttid> = query.$onlyIds ? [] : {}
|
|
84
|
-
|
|
85
|
-
for await (const data of this.findDocs(selCol! as string, query).collect()) {
|
|
86
|
-
if (typeof data === 'object') docs = Object.appendGroup(docs, data)
|
|
87
|
-
else (docs as Array<_ttid>).push(data as _ttid)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return docs
|
|
91
|
-
}
|
|
92
|
-
case 'INSERT': {
|
|
93
|
-
const insert = Parser.parse<T>(SQL) as _storeInsert<T>
|
|
94
|
-
const insCol = insert.$collection
|
|
95
|
-
delete insert.$collection
|
|
96
|
-
return await this.putData(insCol!, insert.$values)
|
|
97
|
-
}
|
|
98
|
-
case 'UPDATE': {
|
|
99
|
-
const update = Parser.parse<T>(SQL) as _storeUpdate<T>
|
|
100
|
-
const updateCol = update.$collection
|
|
101
|
-
delete update.$collection
|
|
102
|
-
return await this.patchDocs(updateCol!, update)
|
|
103
|
-
}
|
|
104
|
-
case 'DELETE': {
|
|
105
|
-
const del = Parser.parse<T>(SQL) as _storeDelete<T>
|
|
106
|
-
const delCol = del.$collection
|
|
107
|
-
delete del.$collection
|
|
108
|
-
return await this.delDocs(delCol!, del)
|
|
109
|
-
}
|
|
110
|
-
default:
|
|
111
|
-
throw new Error('Invalid Operation')
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Creates a new collection on the configured filesystem root.
|
|
117
|
-
* @param collection The name of the collection.
|
|
118
|
-
*/
|
|
119
|
-
static async createCollection(collection: string) {
|
|
120
|
-
await Fylo.defaultEngine.createCollection(collection)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Drops an existing collection from the configured filesystem root.
|
|
125
|
-
* @param collection The name of the collection.
|
|
126
|
-
*/
|
|
127
|
-
static async dropCollection(collection: string) {
|
|
128
|
-
await Fylo.defaultEngine.dropCollection(collection)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async createCollection(collection: string) {
|
|
132
|
-
return await this.engine.createCollection(collection)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async dropCollection(collection: string) {
|
|
136
|
-
return await this.engine.dropCollection(collection)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Loads encrypted field config from a collection's JSON schema if not already loaded.
|
|
141
|
-
* Reads the `$encrypted` array from the schema and registers fields with Cipher.
|
|
142
|
-
* Auto-configures the Cipher key from `ENCRYPTION_KEY` env var on first use.
|
|
143
|
-
*/
|
|
144
|
-
private static async loadEncryption(collection: string): Promise<void> {
|
|
145
|
-
if (Fylo.loadedEncryption.has(collection)) return
|
|
146
|
-
Fylo.loadedEncryption.add(collection)
|
|
147
|
-
|
|
148
|
-
if (!Fylo.SCHEMA_DIR) return
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const res = await import(`${Fylo.SCHEMA_DIR}/${collection}.json`)
|
|
152
|
-
const schema = res.default as Record<string, unknown>
|
|
153
|
-
const encrypted = schema.$encrypted
|
|
154
|
-
|
|
155
|
-
if (Array.isArray(encrypted) && encrypted.length > 0) {
|
|
156
|
-
if (!Cipher.isConfigured()) {
|
|
157
|
-
const secret = process.env.ENCRYPTION_KEY
|
|
158
|
-
if (!secret)
|
|
159
|
-
throw new Error(
|
|
160
|
-
'Schema declares $encrypted fields but ENCRYPTION_KEY env var is not set'
|
|
161
|
-
)
|
|
162
|
-
if (secret.length < 32)
|
|
163
|
-
throw new Error('ENCRYPTION_KEY must be at least 32 characters long')
|
|
164
|
-
await Cipher.configure(secret)
|
|
165
|
-
}
|
|
166
|
-
Cipher.registerFields(collection, encrypted as string[])
|
|
167
|
-
}
|
|
168
|
-
} catch {
|
|
169
|
-
// No schema file found — no encryption for this collection
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Compatibility helper. FYLO now writes synchronously to the filesystem,
|
|
175
|
-
* so there is no queued transactional rollback path to execute.
|
|
176
|
-
*/
|
|
177
|
-
async rollback() {}
|
|
178
|
-
|
|
179
|
-
getDoc<T extends Record<string, any>>(collection: string, _id: _ttid, onlyId: boolean = false) {
|
|
180
|
-
return this.engine.getDoc<T>(collection, _id, onlyId)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
findDocs<T extends Record<string, any>>(collection: string, query?: _storeQuery<T>) {
|
|
184
|
-
return this.engine.findDocs<T>(collection, query)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
|
|
188
|
-
join: _join<T, U>
|
|
189
|
-
) {
|
|
190
|
-
return await this.engine.joinDocs(join)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async *exportBulkData<T extends Record<string, any>>(collection: string) {
|
|
194
|
-
yield* this.engine.exportBulkData<T>(collection)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private unsupportedLegacyApi(feature: string): never {
|
|
198
|
-
throw new Error(
|
|
199
|
-
`${feature} was removed. FYLO now writes synchronously to the filesystem and expects external sync tooling for cloud replication.`
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async getJobStatus(_jobId: string) {
|
|
204
|
-
return this.unsupportedLegacyApi('getJobStatus')
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async getDocStatus(_collection: string, _docId: _ttid) {
|
|
208
|
-
return this.unsupportedLegacyApi('getDocStatus')
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async getDeadLetters(_count: number = 10) {
|
|
212
|
-
return this.unsupportedLegacyApi('getDeadLetters')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async getQueueStats() {
|
|
216
|
-
return this.unsupportedLegacyApi('getQueueStats')
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async replayDeadLetter(_streamId: string) {
|
|
220
|
-
return this.unsupportedLegacyApi('replayDeadLetter')
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async processQueuedWrites(_count: number = 1, _recover: boolean = false) {
|
|
224
|
-
return this.unsupportedLegacyApi('processQueuedWrites')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Imports data from a URL into a collection.
|
|
229
|
-
* @param collection The name of the collection.
|
|
230
|
-
* @param url The URL of the data to import.
|
|
231
|
-
* @param limit The maximum number of documents to import.
|
|
232
|
-
*/
|
|
233
|
-
async importBulkData<T extends Record<string, any>>(
|
|
234
|
-
collection: string,
|
|
235
|
-
url: URL,
|
|
236
|
-
limit?: number
|
|
237
|
-
) {
|
|
238
|
-
const res = await fetch(url)
|
|
239
|
-
|
|
240
|
-
if (!res.headers.get('content-type')?.includes('application/json'))
|
|
241
|
-
throw new Error('Response is not JSON')
|
|
242
|
-
|
|
243
|
-
let count = 0
|
|
244
|
-
let batchNum = 0
|
|
245
|
-
|
|
246
|
-
const flush = async (batch: T[]) => {
|
|
247
|
-
if (!batch.length) return
|
|
248
|
-
|
|
249
|
-
const items =
|
|
250
|
-
limit && count + batch.length > limit ? batch.slice(0, limit - count) : batch
|
|
251
|
-
|
|
252
|
-
batchNum++
|
|
253
|
-
|
|
254
|
-
const start = Date.now()
|
|
255
|
-
await this.batchPutData(collection, items)
|
|
256
|
-
count += items.length
|
|
257
|
-
|
|
258
|
-
if (count % 10000 === 0) console.log('Count:', count)
|
|
259
|
-
|
|
260
|
-
if (Fylo.LOGGING) {
|
|
261
|
-
const bytes = JSON.stringify(items).length
|
|
262
|
-
const elapsed = Date.now() - start
|
|
263
|
-
const bytesPerSec = (bytes / (elapsed / 1000)).toFixed(2)
|
|
264
|
-
console.log(
|
|
265
|
-
`Batch ${batchNum} of ${bytes} bytes took ${elapsed === Infinity ? 'Infinity' : elapsed}ms (${bytesPerSec} bytes/sec)`
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
let isJsonArray: boolean | null = null
|
|
271
|
-
const jsonArrayChunks: Uint8Array[] = []
|
|
272
|
-
let jsonArrayLength = 0
|
|
273
|
-
|
|
274
|
-
let pending = new Uint8Array(0)
|
|
275
|
-
let batch: T[] = []
|
|
276
|
-
|
|
277
|
-
for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) {
|
|
278
|
-
if (isJsonArray === null) isJsonArray = chunk[0] === 0x5b
|
|
279
|
-
|
|
280
|
-
if (isJsonArray) {
|
|
281
|
-
jsonArrayChunks.push(chunk)
|
|
282
|
-
jsonArrayLength += chunk.length
|
|
283
|
-
continue
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const merged = new Uint8Array(pending.length + chunk.length)
|
|
287
|
-
merged.set(pending)
|
|
288
|
-
merged.set(chunk, pending.length)
|
|
289
|
-
|
|
290
|
-
const { values, read } = Bun.JSONL.parseChunk(merged)
|
|
291
|
-
pending = merged.subarray(read)
|
|
292
|
-
|
|
293
|
-
for (const item of values) {
|
|
294
|
-
batch.push(item as T)
|
|
295
|
-
if (batch.length === Fylo.MAX_CPUS) {
|
|
296
|
-
await flush(batch)
|
|
297
|
-
batch = []
|
|
298
|
-
if (limit && count >= limit) return count
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (isJsonArray) {
|
|
304
|
-
const body = new Uint8Array(jsonArrayLength)
|
|
305
|
-
let offset = 0
|
|
306
|
-
for (const c of jsonArrayChunks) {
|
|
307
|
-
body.set(c, offset)
|
|
308
|
-
offset += c.length
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const data = JSON.parse(new TextDecoder().decode(body))
|
|
312
|
-
const items: T[] = Array.isArray(data) ? data : [data]
|
|
313
|
-
|
|
314
|
-
for (let i = 0; i < items.length; i += Fylo.MAX_CPUS) {
|
|
315
|
-
if (limit && count >= limit) break
|
|
316
|
-
await flush(items.slice(i, i + Fylo.MAX_CPUS))
|
|
317
|
-
}
|
|
318
|
-
} else {
|
|
319
|
-
if (pending.length > 0) {
|
|
320
|
-
const { values } = Bun.JSONL.parseChunk(pending)
|
|
321
|
-
for (const item of values) batch.push(item as T)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (batch.length > 0) await flush(batch)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return count
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Gets an exported stream of documents from a collection.
|
|
332
|
-
*/
|
|
333
|
-
static async *exportBulkData<T extends Record<string, any>>(collection: string) {
|
|
334
|
-
yield* Fylo.defaultEngine.exportBulkData<T>(collection)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Gets a document from a collection.
|
|
339
|
-
* @param collection The name of the collection.
|
|
340
|
-
* @param _id The ID of the document.
|
|
341
|
-
* @param onlyId Whether to only return the ID of the document.
|
|
342
|
-
* @returns The document or the ID of the document.
|
|
343
|
-
*/
|
|
344
|
-
static getDoc<T extends Record<string, any>>(
|
|
345
|
-
collection: string,
|
|
346
|
-
_id: _ttid,
|
|
347
|
-
onlyId: boolean = false
|
|
348
|
-
) {
|
|
349
|
-
return Fylo.defaultEngine.getDoc<T>(collection, _id, onlyId)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Puts multiple documents into a collection.
|
|
354
|
-
* @param collection The name of the collection.
|
|
355
|
-
* @param batch The documents to put.
|
|
356
|
-
* @returns The IDs of the documents.
|
|
357
|
-
*/
|
|
358
|
-
async batchPutData<T extends Record<string, any>>(collection: string, batch: Array<T>) {
|
|
359
|
-
const batches: Array<Array<T>> = []
|
|
360
|
-
const ids: _ttid[] = []
|
|
361
|
-
|
|
362
|
-
if (batch.length > navigator.hardwareConcurrency) {
|
|
363
|
-
for (let i = 0; i < batch.length; i += navigator.hardwareConcurrency) {
|
|
364
|
-
batches.push(batch.slice(i, i + navigator.hardwareConcurrency))
|
|
365
|
-
}
|
|
366
|
-
} else batches.push(batch)
|
|
367
|
-
|
|
368
|
-
for (const itemBatch of batches) {
|
|
369
|
-
const res = await Promise.allSettled(
|
|
370
|
-
itemBatch.map((data) => this.putData(collection, data))
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
for (const _id of res
|
|
374
|
-
.filter((item) => item.status === 'fulfilled')
|
|
375
|
-
.map((item) => item.value)) {
|
|
376
|
-
ids.push(_id)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return ids
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async queuePutData<T extends Record<string, any>>(
|
|
384
|
-
_collection: string,
|
|
385
|
-
_data: Record<_ttid, T> | T
|
|
386
|
-
) {
|
|
387
|
-
return this.unsupportedLegacyApi('queuePutData')
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async queuePatchDoc<T extends Record<string, any>>(
|
|
391
|
-
_collection: string,
|
|
392
|
-
_newDoc: Record<_ttid, Partial<T>>,
|
|
393
|
-
_oldDoc: Record<_ttid, T> = {}
|
|
394
|
-
) {
|
|
395
|
-
return this.unsupportedLegacyApi('queuePatchDoc')
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async queueDelDoc(_collection: string, _id: _ttid) {
|
|
399
|
-
return this.unsupportedLegacyApi('queueDelDoc')
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Puts a document into a collection.
|
|
404
|
-
* @param collection The name of the collection.
|
|
405
|
-
* @param data The document to put.
|
|
406
|
-
* @returns The ID of the document.
|
|
407
|
-
*/
|
|
408
|
-
private static async uniqueTTID(existingId?: string): Promise<_ttid> {
|
|
409
|
-
let _id!: _ttid
|
|
410
|
-
const prev = Fylo.ttidLock
|
|
411
|
-
Fylo.ttidLock = prev.then(async () => {
|
|
412
|
-
_id = existingId ? TTID.generate(existingId) : TTID.generate()
|
|
413
|
-
})
|
|
414
|
-
await Fylo.ttidLock
|
|
415
|
-
|
|
416
|
-
return _id
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private async prepareInsert<T extends Record<string, any>>(
|
|
420
|
-
collection: string,
|
|
421
|
-
data: Record<_ttid, T> | T
|
|
422
|
-
) {
|
|
423
|
-
await Fylo.loadEncryption(collection)
|
|
424
|
-
|
|
425
|
-
const currId = Object.keys(data).shift()!
|
|
426
|
-
const _id = TTID.isTTID(currId)
|
|
427
|
-
? await Fylo.uniqueTTID(currId)
|
|
428
|
-
: await Fylo.uniqueTTID(undefined)
|
|
429
|
-
|
|
430
|
-
let doc = TTID.isTTID(currId) ? (Object.values(data).shift() as T) : (data as T)
|
|
431
|
-
|
|
432
|
-
if (Fylo.STRICT) doc = (await Gen.validateData(collection, doc)) as T
|
|
433
|
-
|
|
434
|
-
return { _id, doc }
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
private async executePutDataDirect<T extends Record<string, any>>(
|
|
438
|
-
collection: string,
|
|
439
|
-
_id: _ttid,
|
|
440
|
-
doc: T
|
|
441
|
-
) {
|
|
442
|
-
await this.engine.putDocument(collection, _id, doc)
|
|
443
|
-
|
|
444
|
-
if (Fylo.LOGGING) console.log(`Finished Writing ${_id}`)
|
|
445
|
-
|
|
446
|
-
return _id
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
private async executePatchDocDirect<T extends Record<string, any>>(
|
|
450
|
-
collection: string,
|
|
451
|
-
newDoc: Record<_ttid, Partial<T>>,
|
|
452
|
-
oldDoc: Record<_ttid, T> = {}
|
|
453
|
-
) {
|
|
454
|
-
await Fylo.loadEncryption(collection)
|
|
455
|
-
|
|
456
|
-
const _id = Object.keys(newDoc).shift() as _ttid
|
|
457
|
-
|
|
458
|
-
if (!_id) throw new Error('this document does not contain an TTID')
|
|
459
|
-
|
|
460
|
-
let existingDoc = oldDoc[_id]
|
|
461
|
-
if (!existingDoc) {
|
|
462
|
-
const existing = await this.engine.getDoc<T>(collection, _id).once()
|
|
463
|
-
existingDoc = existing[_id]
|
|
464
|
-
}
|
|
465
|
-
if (!existingDoc) return _id
|
|
466
|
-
|
|
467
|
-
const currData = { ...existingDoc, ...newDoc[_id] } as T
|
|
468
|
-
let docToWrite: T = currData
|
|
469
|
-
const _newId = await Fylo.uniqueTTID(_id)
|
|
470
|
-
if (Fylo.STRICT) docToWrite = (await Gen.validateData(collection, currData)) as T
|
|
471
|
-
|
|
472
|
-
const nextId = await this.engine.patchDocument(
|
|
473
|
-
collection,
|
|
474
|
-
_id,
|
|
475
|
-
_newId,
|
|
476
|
-
docToWrite,
|
|
477
|
-
existingDoc
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
if (Fylo.LOGGING) console.log(`Finished Updating ${_id} to ${nextId}`)
|
|
481
|
-
|
|
482
|
-
return nextId
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
private async executeDelDocDirect(collection: string, _id: _ttid) {
|
|
486
|
-
await this.engine.deleteDocument(collection, _id)
|
|
487
|
-
|
|
488
|
-
if (Fylo.LOGGING) console.log(`Finished Deleting ${_id}`)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async putData<T extends Record<string, any>>(collection: string, data: T): Promise<_ttid>
|
|
492
|
-
async putData<T extends Record<string, any>>(
|
|
493
|
-
collection: string,
|
|
494
|
-
data: Record<_ttid, T>
|
|
495
|
-
): Promise<_ttid>
|
|
496
|
-
async putData<T extends Record<string, any>>(
|
|
497
|
-
collection: string,
|
|
498
|
-
data: Record<_ttid, T> | T,
|
|
499
|
-
options: { wait?: boolean; timeoutMs?: number } = {}
|
|
500
|
-
): Promise<_ttid> {
|
|
501
|
-
if (options.wait === false) {
|
|
502
|
-
this.unsupportedLegacyApi('putData(..., { wait: false })')
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const { _id, doc } = await this.prepareInsert(collection, data)
|
|
506
|
-
await this.executePutDataDirect(collection, _id, doc)
|
|
507
|
-
return _id
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Patches a document in a collection.
|
|
512
|
-
* @param collection The name of the collection.
|
|
513
|
-
* @param newDoc The new document data.
|
|
514
|
-
* @param oldDoc The old document data.
|
|
515
|
-
* @returns The number of documents patched.
|
|
516
|
-
*/
|
|
517
|
-
async patchDoc<T extends Record<string, any>>(
|
|
518
|
-
collection: string,
|
|
519
|
-
newDoc: Record<_ttid, Partial<T>>,
|
|
520
|
-
oldDoc: Record<_ttid, T> = {},
|
|
521
|
-
options: { wait?: boolean; timeoutMs?: number } = {}
|
|
522
|
-
): Promise<_ttid> {
|
|
523
|
-
if (options.wait === false) {
|
|
524
|
-
this.unsupportedLegacyApi('patchDoc(..., { wait: false })')
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
return await this.executePatchDocDirect(collection, newDoc, oldDoc)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Patches documents in a collection.
|
|
532
|
-
* @param collection The name of the collection.
|
|
533
|
-
* @param updateSchema The update schema.
|
|
534
|
-
* @returns The number of documents patched.
|
|
535
|
-
*/
|
|
536
|
-
async patchDocs<T extends Record<string, any>>(
|
|
537
|
-
collection: string,
|
|
538
|
-
updateSchema: _storeUpdate<T>
|
|
539
|
-
) {
|
|
540
|
-
await Fylo.loadEncryption(collection)
|
|
541
|
-
|
|
542
|
-
let count = 0
|
|
543
|
-
const promises: Promise<_ttid>[] = []
|
|
544
|
-
|
|
545
|
-
for await (const value of this.findDocs<T>(collection, updateSchema.$where).collect()) {
|
|
546
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
547
|
-
const [_id, current] = Object.entries(value as Record<_ttid, T>)[0] ?? []
|
|
548
|
-
if (_id && current) {
|
|
549
|
-
promises.push(
|
|
550
|
-
this.patchDoc(collection, { [_id]: updateSchema.$set }, { [_id]: current })
|
|
551
|
-
)
|
|
552
|
-
count++
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
await Promise.all(promises)
|
|
558
|
-
|
|
559
|
-
return count
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Deletes a document from a collection.
|
|
564
|
-
* @param collection The name of the collection.
|
|
565
|
-
* @param _id The ID of the document.
|
|
566
|
-
* @returns The number of documents deleted.
|
|
567
|
-
*/
|
|
568
|
-
async delDoc(
|
|
569
|
-
collection: string,
|
|
570
|
-
_id: _ttid,
|
|
571
|
-
options: { wait?: boolean; timeoutMs?: number } = {}
|
|
572
|
-
): Promise<void> {
|
|
573
|
-
if (options.wait === false) {
|
|
574
|
-
this.unsupportedLegacyApi('delDoc(..., { wait: false })')
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
await this.executeDelDocDirect(collection, _id)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Deletes documents from a collection.
|
|
582
|
-
* @param collection The name of the collection.
|
|
583
|
-
* @param deleteSchema The delete schema.
|
|
584
|
-
* @returns The number of documents deleted.
|
|
585
|
-
*/
|
|
586
|
-
async delDocs<T extends Record<string, any>>(
|
|
587
|
-
collection: string,
|
|
588
|
-
deleteSchema?: _storeDelete<T>
|
|
589
|
-
) {
|
|
590
|
-
await Fylo.loadEncryption(collection)
|
|
591
|
-
|
|
592
|
-
let count = 0
|
|
593
|
-
const promises: Promise<void>[] = []
|
|
594
|
-
|
|
595
|
-
for await (const value of this.findDocs<T>(collection, deleteSchema).collect()) {
|
|
596
|
-
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
597
|
-
const _id = Object.keys(value as Record<_ttid, T>).find((docId) =>
|
|
598
|
-
TTID.isTTID(docId)
|
|
599
|
-
)
|
|
600
|
-
if (_id) {
|
|
601
|
-
promises.push(this.delDoc(collection, _id))
|
|
602
|
-
count++
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
await Promise.all(promises)
|
|
608
|
-
|
|
609
|
-
return count
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Joins documents from two collections.
|
|
614
|
-
* @param join The join schema.
|
|
615
|
-
* @returns The joined documents.
|
|
616
|
-
*/
|
|
617
|
-
static async joinDocs<T extends Record<string, any>, U extends Record<string, any>>(
|
|
618
|
-
join: _join<T, U>
|
|
619
|
-
) {
|
|
620
|
-
return await Fylo.defaultEngine.joinDocs(join)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Finds documents in a collection.
|
|
625
|
-
* @param collection The name of the collection.
|
|
626
|
-
* @param query The query schema.
|
|
627
|
-
* @returns The found documents.
|
|
628
|
-
*/
|
|
629
|
-
static findDocs<T extends Record<string, any>>(collection: string, query?: _storeQuery<T>) {
|
|
630
|
-
return Fylo.defaultEngine.findDocs<T>(collection, query)
|
|
631
|
-
}
|
|
632
|
-
}
|
package/src/sync.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export type FyloSyncMode = 'await-sync' | 'fire-and-forget'
|
|
2
|
-
|
|
3
|
-
export interface FyloWriteSyncEvent<T extends Record<string, any> = Record<string, any>> {
|
|
4
|
-
operation: 'put' | 'patch'
|
|
5
|
-
collection: string
|
|
6
|
-
docId: _ttid
|
|
7
|
-
previousDocId?: _ttid
|
|
8
|
-
path: string
|
|
9
|
-
data: T
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface FyloDeleteSyncEvent {
|
|
13
|
-
operation: 'delete' | 'patch'
|
|
14
|
-
collection: string
|
|
15
|
-
docId: _ttid
|
|
16
|
-
path: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface FyloSyncHooks<T extends Record<string, any> = Record<string, any>> {
|
|
20
|
-
onWrite?: (event: FyloWriteSyncEvent<T>) => Promise<void> | void
|
|
21
|
-
onDelete?: (event: FyloDeleteSyncEvent) => Promise<void> | void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface FyloOptions<T extends Record<string, any> = Record<string, any>> {
|
|
25
|
-
root?: string
|
|
26
|
-
s3FilesRoot?: string
|
|
27
|
-
sync?: FyloSyncHooks<T>
|
|
28
|
-
syncMode?: FyloSyncMode
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class FyloSyncError extends Error {
|
|
32
|
-
readonly collection: string
|
|
33
|
-
readonly docId: _ttid
|
|
34
|
-
readonly path: string
|
|
35
|
-
readonly operation: string
|
|
36
|
-
|
|
37
|
-
constructor(args: {
|
|
38
|
-
collection: string
|
|
39
|
-
docId: _ttid
|
|
40
|
-
path: string
|
|
41
|
-
operation: string
|
|
42
|
-
cause: unknown
|
|
43
|
-
}) {
|
|
44
|
-
super(
|
|
45
|
-
`FYLO sync failed after the local filesystem operation succeeded for ${args.operation} ${args.collection}/${args.docId}. Local state is already committed at ${args.path}.`,
|
|
46
|
-
{ cause: args.cause }
|
|
47
|
-
)
|
|
48
|
-
this.name = 'FyloSyncError'
|
|
49
|
-
this.collection = args.collection
|
|
50
|
-
this.docId = args.docId
|
|
51
|
-
this.path = args.path
|
|
52
|
-
this.operation = args.operation
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function resolveSyncMode(syncMode?: FyloSyncMode): FyloSyncMode {
|
|
57
|
-
return syncMode ?? 'await-sync'
|
|
58
|
-
}
|