@hozon/store-blob 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/lib/api.js +153 -3
- package/lib/index.d.ts +2 -2
- package/lib/migrations.js +23 -0
- package/lib/tables.d.ts +7 -0
- package/lib/types.d.ts +30 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -28,15 +28,22 @@ Entries map integer flags to booleans and expose camelCase fields.
|
|
|
28
28
|
Encryption and key IDs are optional metadata that the store does not interpret.
|
|
29
29
|
Repeated entry and manifest inserts preserve existing rows.
|
|
30
30
|
|
|
31
|
-
For a known remote entry, `beginTransfer` records its manifest
|
|
32
|
-
Retries with the same chunk size preserve manifest digests
|
|
31
|
+
For a known remote entry, `beginTransfer({ blobID, chunkSize, chunks, stagingID })` records its manifest, sets its state to `partial`, and records a transfer session for `stagingID`.
|
|
32
|
+
Retries with the same chunk size preserve manifest digests. Progress is preserved only while `stagingID` matches the session; a different `stagingID` clears it.
|
|
33
|
+
`getTransfer`, `getTransferByStagingID`, and `touchTransfer` read and refresh the session.
|
|
33
34
|
Changing chunk size while a manifest exists rejects with `Cannot change chunk size for blob <blobID>: manifest already exists`.
|
|
34
35
|
Record each verified chunk with `recordTransferChunk`, then call `finalizeTransfer` when all chunks are present.
|
|
35
|
-
Finalisation preserves the manifest, clears transfer progress, and sets the entry state to `local`.
|
|
36
|
+
Finalisation preserves the manifest, clears transfer progress and the session, and sets the entry state to `local`.
|
|
36
37
|
`beginTransfer`, `recordTransferChunk`, and `finalizeTransfer` reject absent entries with `Blob entry <blobID> not found`.
|
|
37
38
|
`recordTransferChunk` rejects unknown manifest indexes with `Blob chunk <blobID> at index <index> not found`.
|
|
38
39
|
Finalisation rejects missing chunks with `Cannot finalize transfer <blobID>: <count> chunk(s) missing`.
|
|
39
|
-
`deleteEntry` removes the entry, manifest,
|
|
40
|
+
`deleteEntry` removes the entry, manifest, transfer progress, and session together.
|
|
41
|
+
Entries carry an optional `contentType` (`string | null`).
|
|
42
|
+
`listEntries({ limit, cursor })` pages entries by `(createdAt, blobID)` with an opaque cursor; `limit` is 1 to 1000.
|
|
43
|
+
`promoteEntry(entry, chunks)` atomically replaces an entry's manifest and marks it `local`, keeping `pinned` and `createdAt` and clearing transfers and session.
|
|
44
|
+
`fillContentType(blobID, contentType)` sets the content type only when the stored one is null.
|
|
45
|
+
`resetTransfer(blobID)` atomically drops the session, progress, and manifest and sets the entry to `remote-only`.
|
|
46
|
+
The `1-sessions` migration resets existing `partial` entries to `remote-only`, dropping their manifests; this is not reversible.
|
|
40
47
|
Manifest chunks reference entries, and transfer rows reference manifest chunks, with cascading deletion preventing orphan progress.
|
|
41
48
|
|
|
42
49
|
Tables follow the database's configured prefix, which defaults to `hozon`.
|
package/lib/api.js
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { chunk, withStoreTransaction } from '@hozon/db';
|
|
2
|
+
import { b64uFromUTF, b64uToUTF } from '@sozai/codec';
|
|
3
|
+
function encodeCursor(createdAt, blobID) {
|
|
4
|
+
return b64uFromUTF(JSON.stringify([
|
|
5
|
+
createdAt,
|
|
6
|
+
blobID
|
|
7
|
+
]));
|
|
8
|
+
}
|
|
9
|
+
function decodeCursor(cursor) {
|
|
10
|
+
try {
|
|
11
|
+
const value = JSON.parse(b64uToUTF(cursor));
|
|
12
|
+
if (Array.isArray(value) && value.length === 2 && typeof value[0] === 'number' && Number.isFinite(value[0]) && typeof value[1] === 'string') {
|
|
13
|
+
return [
|
|
14
|
+
value[0],
|
|
15
|
+
value[1]
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// fall through
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Invalid cursor');
|
|
22
|
+
}
|
|
2
23
|
export function createBlobStoreAPI(db, adapter) {
|
|
3
24
|
async function insertManifest(trx, blobID, chunks) {
|
|
4
25
|
const rows = chunks.map((item)=>({
|
|
@@ -25,7 +46,8 @@ export function createBlobStoreAPI(db, adapter) {
|
|
|
25
46
|
chunk_size: entry.chunkSize,
|
|
26
47
|
state: entry.state,
|
|
27
48
|
pinned: entry.pinned ? 1 : 0,
|
|
28
|
-
created_at: entry.createdAt
|
|
49
|
+
created_at: entry.createdAt,
|
|
50
|
+
content_type: entry.contentType ?? null
|
|
29
51
|
}).onConflict((oc)=>oc.column('blob_id').doNothing()).execute();
|
|
30
52
|
await insertManifest(trx, entry.blobID, chunks);
|
|
31
53
|
});
|
|
@@ -40,7 +62,8 @@ export function createBlobStoreAPI(db, adapter) {
|
|
|
40
62
|
chunkSize: row.chunk_size,
|
|
41
63
|
state: row.state,
|
|
42
64
|
pinned: row.pinned === 1,
|
|
43
|
-
createdAt: row.created_at
|
|
65
|
+
createdAt: row.created_at,
|
|
66
|
+
contentType: row.content_type
|
|
44
67
|
};
|
|
45
68
|
},
|
|
46
69
|
async getChunkDigests (blobID) {
|
|
@@ -54,12 +77,13 @@ export function createBlobStoreAPI(db, adapter) {
|
|
|
54
77
|
},
|
|
55
78
|
async deleteEntry (blobID) {
|
|
56
79
|
await withStoreTransaction(db, async (trx)=>{
|
|
80
|
+
await trx.deleteFrom('blob_transfer_sessions').where('blob_id', '=', blobID).execute();
|
|
57
81
|
await trx.deleteFrom('blob_transfers').where('blob_id', '=', blobID).execute();
|
|
58
82
|
await trx.deleteFrom('blob_chunks').where('blob_id', '=', blobID).execute();
|
|
59
83
|
await trx.deleteFrom('blob_entries').where('blob_id', '=', blobID).execute();
|
|
60
84
|
});
|
|
61
85
|
},
|
|
62
|
-
async beginTransfer (blobID, chunkSize, chunks) {
|
|
86
|
+
async beginTransfer ({ blobID, chunkSize, chunks, stagingID }) {
|
|
63
87
|
await withStoreTransaction(db, async (trx)=>{
|
|
64
88
|
let query = trx.selectFrom('blob_entries').select([
|
|
65
89
|
'blob_id',
|
|
@@ -79,6 +103,20 @@ export function createBlobStoreAPI(db, adapter) {
|
|
|
79
103
|
state: 'partial'
|
|
80
104
|
}).where('blob_id', '=', blobID).execute();
|
|
81
105
|
await insertManifest(trx, blobID, chunks);
|
|
106
|
+
const existing = await trx.selectFrom('blob_transfer_sessions').select('staging_id').where('blob_id', '=', blobID).executeTakeFirst();
|
|
107
|
+
// A new staging area holds none of the previous session's chunks.
|
|
108
|
+
if (existing !== undefined && existing.staging_id !== stagingID) {
|
|
109
|
+
await trx.deleteFrom('blob_transfers').where('blob_id', '=', blobID).execute();
|
|
110
|
+
}
|
|
111
|
+
const updatedAt = Date.now();
|
|
112
|
+
await trx.insertInto('blob_transfer_sessions').values({
|
|
113
|
+
blob_id: blobID,
|
|
114
|
+
staging_id: stagingID,
|
|
115
|
+
updated_at: updatedAt
|
|
116
|
+
}).onConflict((oc)=>oc.column('blob_id').doUpdateSet({
|
|
117
|
+
staging_id: stagingID,
|
|
118
|
+
updated_at: updatedAt
|
|
119
|
+
})).execute();
|
|
82
120
|
});
|
|
83
121
|
},
|
|
84
122
|
async recordTransferChunk (blobID, index) {
|
|
@@ -94,6 +132,9 @@ export function createBlobStoreAPI(db, adapter) {
|
|
|
94
132
|
'blob_id',
|
|
95
133
|
'index'
|
|
96
134
|
]).doNothing()).execute();
|
|
135
|
+
await trx.updateTable('blob_transfer_sessions').set({
|
|
136
|
+
updated_at: Date.now()
|
|
137
|
+
}).where('blob_id', '=', blobID).execute();
|
|
97
138
|
});
|
|
98
139
|
},
|
|
99
140
|
async getPresentChunkIndexes (blobID) {
|
|
@@ -115,7 +156,116 @@ export function createBlobStoreAPI(db, adapter) {
|
|
|
115
156
|
state: 'local'
|
|
116
157
|
}).where('blob_id', '=', blobID).execute();
|
|
117
158
|
await trx.deleteFrom('blob_transfers').where('blob_id', '=', blobID).execute();
|
|
159
|
+
await trx.deleteFrom('blob_transfer_sessions').where('blob_id', '=', blobID).execute();
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
async getTransfer (blobID) {
|
|
163
|
+
const session = await db.selectFrom('blob_transfer_sessions').select([
|
|
164
|
+
'staging_id',
|
|
165
|
+
'updated_at'
|
|
166
|
+
]).where('blob_id', '=', blobID).executeTakeFirst();
|
|
167
|
+
if (session === undefined) return null;
|
|
168
|
+
const rows = await db.selectFrom('blob_transfers').select('index').where('blob_id', '=', blobID).orderBy('index', 'asc').execute();
|
|
169
|
+
return {
|
|
170
|
+
stagingID: session.staging_id,
|
|
171
|
+
updatedAt: session.updated_at,
|
|
172
|
+
presentChunks: rows.map((row)=>row.index)
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
async getTransferByStagingID (stagingID) {
|
|
176
|
+
const row = await db.selectFrom('blob_transfer_sessions').select([
|
|
177
|
+
'blob_id',
|
|
178
|
+
'updated_at'
|
|
179
|
+
]).where('staging_id', '=', stagingID).executeTakeFirst();
|
|
180
|
+
return row === undefined ? null : {
|
|
181
|
+
blobID: row.blob_id,
|
|
182
|
+
updatedAt: row.updated_at
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
async touchTransfer (blobID) {
|
|
186
|
+
await db.updateTable('blob_transfer_sessions').set({
|
|
187
|
+
updated_at: Date.now()
|
|
188
|
+
}).where('blob_id', '=', blobID).execute();
|
|
189
|
+
},
|
|
190
|
+
async listEntries (params) {
|
|
191
|
+
const { limit, cursor } = params;
|
|
192
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
|
|
193
|
+
throw new Error('Invalid limit: must be an integer between 1 and 1000');
|
|
194
|
+
}
|
|
195
|
+
let query = db.selectFrom('blob_entries').selectAll();
|
|
196
|
+
if (cursor !== undefined) {
|
|
197
|
+
const [createdAt, blobID] = decodeCursor(cursor);
|
|
198
|
+
query = query.where((eb)=>eb.or([
|
|
199
|
+
eb('created_at', '>', createdAt),
|
|
200
|
+
eb.and([
|
|
201
|
+
eb('created_at', '=', createdAt),
|
|
202
|
+
eb('blob_id', '>', blobID)
|
|
203
|
+
])
|
|
204
|
+
]));
|
|
205
|
+
}
|
|
206
|
+
const rows = await query.orderBy('created_at', 'asc').orderBy('blob_id', 'asc').limit(limit + 1).execute();
|
|
207
|
+
const page = rows.slice(0, limit);
|
|
208
|
+
const entries = page.map((row)=>({
|
|
209
|
+
blobID: row.blob_id,
|
|
210
|
+
contentLength: row.content_length,
|
|
211
|
+
encrypted: row.encrypted === 1,
|
|
212
|
+
keyID: row.key_id,
|
|
213
|
+
chunkSize: row.chunk_size,
|
|
214
|
+
state: row.state,
|
|
215
|
+
pinned: row.pinned === 1,
|
|
216
|
+
createdAt: row.created_at,
|
|
217
|
+
contentType: row.content_type
|
|
218
|
+
}));
|
|
219
|
+
const last = page[page.length - 1];
|
|
220
|
+
const nextCursor = rows.length > limit && last !== undefined ? encodeCursor(last.created_at, last.blob_id) : null;
|
|
221
|
+
return {
|
|
222
|
+
entries,
|
|
223
|
+
nextCursor
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
async promoteEntry (entry, chunks) {
|
|
227
|
+
await withStoreTransaction(db, async (trx)=>{
|
|
228
|
+
let lock = trx.selectFrom('blob_entries').select('blob_id').where('blob_id', '=', entry.blobID);
|
|
229
|
+
if (adapter.kind === 'postgres') lock = lock.forUpdate();
|
|
230
|
+
await lock.executeTakeFirst();
|
|
231
|
+
const values = {
|
|
232
|
+
content_length: entry.contentLength,
|
|
233
|
+
encrypted: entry.encrypted ? 1 : 0,
|
|
234
|
+
key_id: entry.keyID ?? null,
|
|
235
|
+
chunk_size: entry.chunkSize,
|
|
236
|
+
state: 'local',
|
|
237
|
+
content_type: entry.contentType ?? null
|
|
238
|
+
};
|
|
239
|
+
await trx.insertInto('blob_entries').values({
|
|
240
|
+
blob_id: entry.blobID,
|
|
241
|
+
pinned: entry.pinned ? 1 : 0,
|
|
242
|
+
created_at: entry.createdAt,
|
|
243
|
+
...values
|
|
244
|
+
}).onConflict((oc)=>oc.column('blob_id').doUpdateSet(values)).execute();
|
|
245
|
+
await trx.deleteFrom('blob_transfer_sessions').where('blob_id', '=', entry.blobID).execute();
|
|
246
|
+
await trx.deleteFrom('blob_transfers').where('blob_id', '=', entry.blobID).execute();
|
|
247
|
+
await trx.deleteFrom('blob_chunks').where('blob_id', '=', entry.blobID).execute();
|
|
248
|
+
await insertManifest(trx, entry.blobID, chunks);
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
async resetTransfer (blobID) {
|
|
252
|
+
await withStoreTransaction(db, async (trx)=>{
|
|
253
|
+
let lock = trx.selectFrom('blob_entries').select('blob_id').where('blob_id', '=', blobID);
|
|
254
|
+
if (adapter.kind === 'postgres') lock = lock.forUpdate();
|
|
255
|
+
const entry = await lock.executeTakeFirst();
|
|
256
|
+
if (entry === undefined) return;
|
|
257
|
+
await trx.deleteFrom('blob_transfer_sessions').where('blob_id', '=', blobID).execute();
|
|
258
|
+
await trx.deleteFrom('blob_transfers').where('blob_id', '=', blobID).execute();
|
|
259
|
+
await trx.deleteFrom('blob_chunks').where('blob_id', '=', blobID).execute();
|
|
260
|
+
await trx.updateTable('blob_entries').set({
|
|
261
|
+
state: 'remote-only'
|
|
262
|
+
}).where('blob_id', '=', blobID).execute();
|
|
118
263
|
});
|
|
264
|
+
},
|
|
265
|
+
async fillContentType (blobID, contentType) {
|
|
266
|
+
await db.updateTable('blob_entries').set({
|
|
267
|
+
content_type: contentType
|
|
268
|
+
}).where('blob_id', '=', blobID).where('content_type', 'is', null).execute();
|
|
119
269
|
}
|
|
120
270
|
};
|
|
121
271
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { BLOB_STORE, blobStoreDefinition, getBlobStore } from './definition.js';
|
|
2
|
-
export type { BlobChunkTable, BlobEntryTable, BlobTables, BlobTransferTable } from './tables.js';
|
|
3
|
-
export type { BlobChunkInput, BlobEntry, BlobEntryInput, BlobState, BlobStoreAPI } from './types.js';
|
|
2
|
+
export type { BlobChunkTable, BlobEntryTable, BlobTables, BlobTransferSessionTable, BlobTransferTable, } from './tables.js';
|
|
3
|
+
export type { BeginTransferParams, BlobChunkInput, BlobEntry, BlobEntryInput, BlobState, BlobStoreAPI, BlobTransfer, } from './types.js';
|
package/lib/migrations.js
CHANGED
|
@@ -28,6 +28,29 @@ export function blobStoreMigrations(ctx) {
|
|
|
28
28
|
await db.schema.dropTable('blob_chunks').execute();
|
|
29
29
|
await db.schema.dropTable('blob_entries').execute();
|
|
30
30
|
}
|
|
31
|
+
},
|
|
32
|
+
'1-sessions': {
|
|
33
|
+
async up (db) {
|
|
34
|
+
await db.schema.alterTable('blob_entries').addColumn('content_type', ctx.types.text).execute();
|
|
35
|
+
await db.schema.createTable('blob_transfer_sessions').addColumn('blob_id', ctx.types.text, (column)=>column.notNull()).addColumn('staging_id', ctx.types.text, (column)=>column.notNull().unique()).addColumn('updated_at', ctx.types.bigint, (column)=>column.notNull()).addPrimaryKeyConstraint(`${ctx.tablePrefix}_blob_transfer_sessions_pkey`, [
|
|
36
|
+
'blob_id'
|
|
37
|
+
]).addForeignKeyConstraint(`${ctx.tablePrefix}_blob_transfer_sessions_entry_fkey`, [
|
|
38
|
+
'blob_id'
|
|
39
|
+
], 'blob_entries', [
|
|
40
|
+
'blob_id'
|
|
41
|
+
], (constraint)=>constraint.onDelete('cascade')).execute();
|
|
42
|
+
// Partial entries predate sessions and cannot be resumed: drop their progress.
|
|
43
|
+
await db.deleteFrom('blob_transfers').where('blob_id', 'in', db.selectFrom('blob_entries').select('blob_id').where('state', '=', 'partial')).execute();
|
|
44
|
+
await db.deleteFrom('blob_chunks').where('blob_id', 'in', db.selectFrom('blob_entries').select('blob_id').where('state', '=', 'partial')).execute();
|
|
45
|
+
await db.updateTable('blob_entries').set({
|
|
46
|
+
state: 'remote-only'
|
|
47
|
+
}).where('state', '=', 'partial').execute();
|
|
48
|
+
},
|
|
49
|
+
// The backfill (partial entries reset to remote-only, manifests dropped) is not reversible.
|
|
50
|
+
async down (db) {
|
|
51
|
+
await db.schema.dropTable('blob_transfer_sessions').execute();
|
|
52
|
+
await db.schema.alterTable('blob_entries').dropColumn('content_type').execute();
|
|
53
|
+
}
|
|
31
54
|
}
|
|
32
55
|
};
|
|
33
56
|
}
|
package/lib/tables.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type BlobEntryTable = {
|
|
|
8
8
|
state: BlobState;
|
|
9
9
|
pinned: number;
|
|
10
10
|
created_at: number;
|
|
11
|
+
content_type: string | null;
|
|
11
12
|
};
|
|
12
13
|
export type BlobChunkTable = {
|
|
13
14
|
blob_id: string;
|
|
@@ -18,8 +19,14 @@ export type BlobTransferTable = {
|
|
|
18
19
|
blob_id: string;
|
|
19
20
|
index: number;
|
|
20
21
|
};
|
|
22
|
+
export type BlobTransferSessionTable = {
|
|
23
|
+
blob_id: string;
|
|
24
|
+
staging_id: string;
|
|
25
|
+
updated_at: number;
|
|
26
|
+
};
|
|
21
27
|
export type BlobTables = {
|
|
22
28
|
blob_entries: BlobEntryTable;
|
|
23
29
|
blob_chunks: BlobChunkTable;
|
|
24
30
|
blob_transfers: BlobTransferTable;
|
|
31
|
+
blob_transfer_sessions: BlobTransferSessionTable;
|
|
25
32
|
};
|
package/lib/types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type BlobEntryInput = {
|
|
|
8
8
|
state: BlobState;
|
|
9
9
|
pinned?: boolean;
|
|
10
10
|
createdAt: number;
|
|
11
|
+
contentType?: string | null;
|
|
11
12
|
};
|
|
12
13
|
export type BlobChunkInput = {
|
|
13
14
|
index: number;
|
|
@@ -22,6 +23,18 @@ export type BlobEntry = {
|
|
|
22
23
|
state: BlobState;
|
|
23
24
|
pinned: boolean;
|
|
24
25
|
createdAt: number;
|
|
26
|
+
contentType: string | null;
|
|
27
|
+
};
|
|
28
|
+
export type BeginTransferParams = {
|
|
29
|
+
blobID: string;
|
|
30
|
+
chunkSize: number;
|
|
31
|
+
chunks: Array<BlobChunkInput>;
|
|
32
|
+
stagingID: string;
|
|
33
|
+
};
|
|
34
|
+
export type BlobTransfer = {
|
|
35
|
+
stagingID: string;
|
|
36
|
+
updatedAt: number;
|
|
37
|
+
presentChunks: Array<number>;
|
|
25
38
|
};
|
|
26
39
|
export type BlobStoreAPI = {
|
|
27
40
|
insertEntry(entry: BlobEntryInput, chunks: Array<BlobChunkInput>): Promise<void>;
|
|
@@ -29,8 +42,24 @@ export type BlobStoreAPI = {
|
|
|
29
42
|
getChunkDigests(blobID: string): Promise<Array<Uint8Array>>;
|
|
30
43
|
setPinned(blobID: string, pinned: boolean): Promise<void>;
|
|
31
44
|
deleteEntry(blobID: string): Promise<void>;
|
|
32
|
-
beginTransfer(
|
|
45
|
+
beginTransfer(params: BeginTransferParams): Promise<void>;
|
|
33
46
|
recordTransferChunk(blobID: string, index: number): Promise<void>;
|
|
34
47
|
getPresentChunkIndexes(blobID: string): Promise<Array<number>>;
|
|
35
48
|
finalizeTransfer(blobID: string): Promise<void>;
|
|
49
|
+
getTransfer(blobID: string): Promise<BlobTransfer | null>;
|
|
50
|
+
getTransferByStagingID(stagingID: string): Promise<{
|
|
51
|
+
blobID: string;
|
|
52
|
+
updatedAt: number;
|
|
53
|
+
} | null>;
|
|
54
|
+
touchTransfer(blobID: string): Promise<void>;
|
|
55
|
+
listEntries(params: {
|
|
56
|
+
limit: number;
|
|
57
|
+
cursor?: string;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
entries: Array<BlobEntry>;
|
|
60
|
+
nextCursor: string | null;
|
|
61
|
+
}>;
|
|
62
|
+
promoteEntry(entry: Omit<BlobEntryInput, 'state'>, chunks: Array<BlobChunkInput>): Promise<void>;
|
|
63
|
+
resetTransfer(blobID: string): Promise<void>;
|
|
64
|
+
fillContentType(blobID: string, contentType: string): Promise<void>;
|
|
36
65
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hozon/store-blob",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Content-addressed blob metadata and resumable transfer store for Hozon",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hozon/adapter": "^0.1.0",
|
|
21
|
-
"@hozon/db": "^0.2.
|
|
21
|
+
"@hozon/db": "^0.2.2",
|
|
22
|
+
"@sozai/codec": "^0.4.2"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@hozon/node-sqlite": "^0.1.0",
|