@graffiti-garden/implementation-local 0.4.1 → 0.4.2
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 +1 -1
- package/dist/browser/ajv-6AI3HK2A.js +9 -0
- package/dist/browser/ajv-6AI3HK2A.js.map +7 -0
- package/dist/browser/chunk-KNUPPOQC.js +7 -0
- package/dist/browser/chunk-KNUPPOQC.js.map +7 -0
- package/dist/browser/fast-json-patch-ZE7SZEYK.js +19 -0
- package/dist/browser/fast-json-patch-ZE7SZEYK.js.map +7 -0
- package/dist/browser/index-browser.es-G37SKL53.js +8 -0
- package/dist/browser/index-browser.es-G37SKL53.js.map +7 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/cjs/database.js +1 -1
- package/dist/cjs/database.js.map +3 -3
- package/dist/database.d.ts +26 -5
- package/dist/database.d.ts.map +1 -1
- package/dist/esm/database.js +1 -1
- package/dist/esm/database.js.map +3 -3
- package/package.json +2 -2
- package/src/database.ts +177 -101
- package/dist/index.browser.js +0 -53
- package/dist/index.browser.js.map +0 -7
package/src/database.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
GraffitiErrorForbidden,
|
|
11
11
|
GraffitiErrorPatchError,
|
|
12
12
|
} from "@graffiti-garden/api";
|
|
13
|
-
import PouchDB from "pouchdb";
|
|
14
13
|
import {
|
|
15
14
|
locationToUri,
|
|
16
15
|
unpackLocationOrUri,
|
|
@@ -22,8 +21,8 @@ import {
|
|
|
22
21
|
compileGraffitiObjectSchema,
|
|
23
22
|
} from "./utilities.js";
|
|
24
23
|
import { Repeater } from "@repeaterjs/repeater";
|
|
25
|
-
import Ajv from "ajv";
|
|
26
|
-
import { applyPatch } from "fast-json-patch";
|
|
24
|
+
import type Ajv from "ajv";
|
|
25
|
+
import type { applyPatch } from "fast-json-patch";
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Constructor options for the GraffitiPoubchDB class.
|
|
@@ -43,6 +42,22 @@ export interface GraffitiLocalOptions {
|
|
|
43
42
|
* Defaults to `"local"`.
|
|
44
43
|
*/
|
|
45
44
|
sourceName?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to allow putting objects with a different than the
|
|
47
|
+
* default source name. Defaults to `false`.
|
|
48
|
+
*
|
|
49
|
+
* Allows this implementation to be used as a client-side cache
|
|
50
|
+
* for remote sources.
|
|
51
|
+
*/
|
|
52
|
+
allowOtherSources?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Whether to allow the user to set the lastModified field
|
|
55
|
+
* when putting objects. Defaults to `false`.
|
|
56
|
+
*
|
|
57
|
+
* Allows this implementation to be used as a client-side cache
|
|
58
|
+
* for remote sources.
|
|
59
|
+
*/
|
|
60
|
+
allowSettinngLastModified?: boolean;
|
|
46
61
|
/**
|
|
47
62
|
* The time in milliseconds to keep tombstones before deleting them.
|
|
48
63
|
* See the {@link https://api.graffiti.garden/classes/Graffiti.html#discover | `discover` }
|
|
@@ -51,10 +66,14 @@ export interface GraffitiLocalOptions {
|
|
|
51
66
|
tombstoneRetention?: number;
|
|
52
67
|
/**
|
|
53
68
|
* An optional Ajv instance to use for schema validation.
|
|
69
|
+
* If not provided, an internal instance will be created.
|
|
54
70
|
*/
|
|
55
71
|
ajv?: Ajv;
|
|
56
72
|
}
|
|
57
73
|
|
|
74
|
+
const DEFAULT_TOMBSTONE_RETENTION = 86400000; // 1 day in milliseconds
|
|
75
|
+
const DEFAULT_SOURCE_NAME = "local";
|
|
76
|
+
|
|
58
77
|
/**
|
|
59
78
|
* An implementation of only the database operations of the
|
|
60
79
|
* GraffitiAPI without synchronization or session management.
|
|
@@ -72,91 +91,121 @@ export class GraffitiLocalDatabase
|
|
|
72
91
|
| "channelStats"
|
|
73
92
|
>
|
|
74
93
|
{
|
|
75
|
-
protected
|
|
76
|
-
protected
|
|
77
|
-
protected
|
|
78
|
-
protected readonly
|
|
94
|
+
protected db_: Promise<PouchDB.Database<GraffitiObjectBase>> | undefined;
|
|
95
|
+
protected applyPatch_: Promise<typeof applyPatch> | undefined;
|
|
96
|
+
protected ajv_: Promise<Ajv> | undefined;
|
|
97
|
+
protected readonly options: GraffitiLocalOptions;
|
|
98
|
+
|
|
99
|
+
get db() {
|
|
100
|
+
if (!this.db_) {
|
|
101
|
+
this.db_ = (async () => {
|
|
102
|
+
const { default: PouchDB } = await import("pouchdb");
|
|
103
|
+
const pouchDbOptions = {
|
|
104
|
+
name: "graffitiDb",
|
|
105
|
+
...this.options.pouchDBOptions,
|
|
106
|
+
};
|
|
107
|
+
const db = new PouchDB<GraffitiObjectBase>(
|
|
108
|
+
pouchDbOptions.name,
|
|
109
|
+
pouchDbOptions,
|
|
110
|
+
);
|
|
111
|
+
await db
|
|
112
|
+
//@ts-ignore
|
|
113
|
+
.put({
|
|
114
|
+
_id: "_design/indexes",
|
|
115
|
+
views: {
|
|
116
|
+
objectsPerChannelAndLastModified: {
|
|
117
|
+
map: function (object: GraffitiObjectBase) {
|
|
118
|
+
const paddedLastModified = object.lastModified
|
|
119
|
+
.toString()
|
|
120
|
+
.padStart(15, "0");
|
|
121
|
+
object.channels.forEach(function (channel) {
|
|
122
|
+
const id =
|
|
123
|
+
encodeURIComponent(channel) + "/" + paddedLastModified;
|
|
124
|
+
//@ts-ignore
|
|
125
|
+
emit(id);
|
|
126
|
+
});
|
|
127
|
+
}.toString(),
|
|
128
|
+
},
|
|
129
|
+
orphansPerActorAndLastModified: {
|
|
130
|
+
map: function (object: GraffitiObjectBase) {
|
|
131
|
+
if (object.channels.length === 0) {
|
|
132
|
+
const paddedLastModified = object.lastModified
|
|
133
|
+
.toString()
|
|
134
|
+
.padStart(15, "0");
|
|
135
|
+
const id =
|
|
136
|
+
encodeURIComponent(object.actor) +
|
|
137
|
+
"/" +
|
|
138
|
+
paddedLastModified;
|
|
139
|
+
//@ts-ignore
|
|
140
|
+
emit(id);
|
|
141
|
+
}
|
|
142
|
+
}.toString(),
|
|
143
|
+
},
|
|
144
|
+
channelStatsPerActor: {
|
|
145
|
+
map: function (object: GraffitiObjectBase) {
|
|
146
|
+
if (object.tombstone) return;
|
|
147
|
+
object.channels.forEach(function (channel) {
|
|
148
|
+
const id =
|
|
149
|
+
encodeURIComponent(object.actor) +
|
|
150
|
+
"/" +
|
|
151
|
+
encodeURIComponent(channel);
|
|
152
|
+
//@ts-ignore
|
|
153
|
+
emit(id, object.lastModified);
|
|
154
|
+
});
|
|
155
|
+
}.toString(),
|
|
156
|
+
reduce: "_stats",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
//@ts-ignore
|
|
161
|
+
.catch((error) => {
|
|
162
|
+
if (
|
|
163
|
+
error &&
|
|
164
|
+
typeof error === "object" &&
|
|
165
|
+
"name" in error &&
|
|
166
|
+
error.name === "conflict"
|
|
167
|
+
) {
|
|
168
|
+
// Design document already exists
|
|
169
|
+
return;
|
|
170
|
+
} else {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return db;
|
|
175
|
+
})();
|
|
176
|
+
}
|
|
177
|
+
return this.db_;
|
|
178
|
+
}
|
|
79
179
|
|
|
80
|
-
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.db = new PouchDB<GraffitiObjectBase>(
|
|
90
|
-
pouchDbOptions.name,
|
|
91
|
-
pouchDbOptions,
|
|
92
|
-
);
|
|
180
|
+
get applyPatch() {
|
|
181
|
+
if (!this.applyPatch_) {
|
|
182
|
+
this.applyPatch_ = (async () => {
|
|
183
|
+
const { applyPatch } = await import("fast-json-patch");
|
|
184
|
+
return applyPatch;
|
|
185
|
+
})();
|
|
186
|
+
}
|
|
187
|
+
return this.applyPatch_;
|
|
188
|
+
}
|
|
93
189
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
encodeURIComponent(channel) + "/" + paddedLastModified;
|
|
107
|
-
//@ts-ignore
|
|
108
|
-
emit(id);
|
|
109
|
-
});
|
|
110
|
-
}.toString(),
|
|
111
|
-
},
|
|
112
|
-
orphansPerActorAndLastModified: {
|
|
113
|
-
map: function (object: GraffitiObjectBase) {
|
|
114
|
-
if (object.channels.length === 0) {
|
|
115
|
-
const paddedLastModified = object.lastModified
|
|
116
|
-
.toString()
|
|
117
|
-
.padStart(15, "0");
|
|
118
|
-
const id =
|
|
119
|
-
encodeURIComponent(object.actor) + "/" + paddedLastModified;
|
|
120
|
-
//@ts-ignore
|
|
121
|
-
emit(id);
|
|
122
|
-
}
|
|
123
|
-
}.toString(),
|
|
124
|
-
},
|
|
125
|
-
channelStatsPerActor: {
|
|
126
|
-
map: function (object: GraffitiObjectBase) {
|
|
127
|
-
if (object.tombstone) return;
|
|
128
|
-
object.channels.forEach(function (channel) {
|
|
129
|
-
const id =
|
|
130
|
-
encodeURIComponent(object.actor) +
|
|
131
|
-
"/" +
|
|
132
|
-
encodeURIComponent(channel);
|
|
133
|
-
//@ts-ignore
|
|
134
|
-
emit(id, object.lastModified);
|
|
135
|
-
});
|
|
136
|
-
}.toString(),
|
|
137
|
-
reduce: "_stats",
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
})
|
|
141
|
-
//@ts-ignore
|
|
142
|
-
.catch((error) => {
|
|
143
|
-
if (
|
|
144
|
-
error &&
|
|
145
|
-
typeof error === "object" &&
|
|
146
|
-
"name" in error &&
|
|
147
|
-
error.name === "conflict"
|
|
148
|
-
) {
|
|
149
|
-
// Design document already exists
|
|
150
|
-
return;
|
|
151
|
-
} else {
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
154
|
-
});
|
|
190
|
+
get ajv() {
|
|
191
|
+
if (!this.ajv_) {
|
|
192
|
+
this.ajv_ = (async () => {
|
|
193
|
+
const { default: Ajv } = await import("ajv");
|
|
194
|
+
return new Ajv({ strict: false });
|
|
195
|
+
})();
|
|
196
|
+
}
|
|
197
|
+
return this.ajv_;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
constructor(options?: GraffitiLocalOptions) {
|
|
201
|
+
this.options = options ?? {};
|
|
155
202
|
}
|
|
156
203
|
|
|
157
204
|
protected async queryByLocation(location: GraffitiLocation) {
|
|
158
205
|
const uri = locationToUri(location) + "/";
|
|
159
|
-
const results = await
|
|
206
|
+
const results = await (
|
|
207
|
+
await this.db
|
|
208
|
+
).allDocs({
|
|
160
209
|
startkey: uri,
|
|
161
210
|
endkey: uri + "\uffff", // \uffff is the last unicode character
|
|
162
211
|
include_docs: true,
|
|
@@ -201,7 +250,7 @@ export class GraffitiLocalDatabase
|
|
|
201
250
|
// if the user is not the owner
|
|
202
251
|
maskGraffitiObject(object, [], session);
|
|
203
252
|
|
|
204
|
-
const validate = compileGraffitiObjectSchema(this.ajv, schema);
|
|
253
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
205
254
|
if (!validate(object)) {
|
|
206
255
|
throw new GraffitiErrorSchemaMismatch();
|
|
207
256
|
}
|
|
@@ -252,7 +301,9 @@ export class GraffitiLocalDatabase
|
|
|
252
301
|
|
|
253
302
|
const lastModified = keepLatest ? latestModified : new Date().getTime();
|
|
254
303
|
|
|
255
|
-
const deleteResults = await
|
|
304
|
+
const deleteResults = await (
|
|
305
|
+
await this.db
|
|
306
|
+
).bulkDocs<GraffitiObjectBase>(
|
|
256
307
|
docsToDelete.map((doc) => ({
|
|
257
308
|
...doc,
|
|
258
309
|
tombstone: true,
|
|
@@ -300,24 +351,37 @@ export class GraffitiLocalDatabase
|
|
|
300
351
|
if (objectPartial.actor && objectPartial.actor !== session.actor) {
|
|
301
352
|
throw new GraffitiErrorForbidden();
|
|
302
353
|
}
|
|
303
|
-
if (
|
|
354
|
+
if (
|
|
355
|
+
objectPartial.source &&
|
|
356
|
+
objectPartial.source !==
|
|
357
|
+
(this.options.sourceName ?? DEFAULT_SOURCE_NAME) &&
|
|
358
|
+
!(this.options.allowOtherSources ?? false)
|
|
359
|
+
) {
|
|
304
360
|
throw new GraffitiErrorForbidden(
|
|
305
361
|
"Putting an object that does not match this source",
|
|
306
362
|
);
|
|
307
363
|
}
|
|
308
364
|
|
|
365
|
+
const lastModified =
|
|
366
|
+
((this.options.allowSettinngLastModified ?? false) &&
|
|
367
|
+
objectPartial.lastModified) ||
|
|
368
|
+
new Date().getTime();
|
|
369
|
+
|
|
309
370
|
const object: GraffitiObjectBase = {
|
|
310
371
|
value: objectPartial.value,
|
|
311
372
|
channels: objectPartial.channels,
|
|
312
373
|
allowed: objectPartial.allowed,
|
|
313
374
|
name: objectPartial.name ?? randomBase64(),
|
|
314
|
-
source:
|
|
375
|
+
source:
|
|
376
|
+
objectPartial.source ?? this.options.sourceName ?? DEFAULT_SOURCE_NAME,
|
|
315
377
|
actor: session.actor,
|
|
316
378
|
tombstone: false,
|
|
317
|
-
lastModified
|
|
379
|
+
lastModified,
|
|
318
380
|
};
|
|
319
381
|
|
|
320
|
-
await
|
|
382
|
+
await (
|
|
383
|
+
await this.db
|
|
384
|
+
).put({
|
|
321
385
|
_id: this.docId(object),
|
|
322
386
|
...object,
|
|
323
387
|
});
|
|
@@ -353,7 +417,7 @@ export class GraffitiLocalDatabase
|
|
|
353
417
|
// Patch it outside of the database
|
|
354
418
|
const patchObject: GraffitiObjectBase = { ...originalObject };
|
|
355
419
|
for (const prop of ["value", "channels", "allowed"] as const) {
|
|
356
|
-
applyGraffitiPatch(applyPatch, prop, patch, patchObject);
|
|
420
|
+
applyGraffitiPatch(await this.applyPatch, prop, patch, patchObject);
|
|
357
421
|
}
|
|
358
422
|
|
|
359
423
|
// Make sure the value is an object
|
|
@@ -387,7 +451,9 @@ export class GraffitiLocalDatabase
|
|
|
387
451
|
}
|
|
388
452
|
|
|
389
453
|
patchObject.lastModified = new Date().getTime();
|
|
390
|
-
await
|
|
454
|
+
await (
|
|
455
|
+
await this.db
|
|
456
|
+
).put({
|
|
391
457
|
...patchObject,
|
|
392
458
|
_id: this.docId(patchObject),
|
|
393
459
|
});
|
|
@@ -451,7 +517,6 @@ export class GraffitiLocalDatabase
|
|
|
451
517
|
|
|
452
518
|
discover: Graffiti["discover"] = (...args) => {
|
|
453
519
|
const [channels, schema, session] = args;
|
|
454
|
-
const validate = compileGraffitiObjectSchema(this.ajv, schema);
|
|
455
520
|
|
|
456
521
|
const { startKeySuffix, endKeySuffix } =
|
|
457
522
|
this.queryLastModifiedSuffixes(schema);
|
|
@@ -459,6 +524,8 @@ export class GraffitiLocalDatabase
|
|
|
459
524
|
const repeater: ReturnType<
|
|
460
525
|
typeof Graffiti.prototype.discover<typeof schema>
|
|
461
526
|
> = new Repeater(async (push, stop) => {
|
|
527
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
528
|
+
|
|
462
529
|
const processedIds = new Set<string>();
|
|
463
530
|
|
|
464
531
|
for (const channel of channels) {
|
|
@@ -466,7 +533,9 @@ export class GraffitiLocalDatabase
|
|
|
466
533
|
const startkey = keyPrefix + startKeySuffix;
|
|
467
534
|
const endkey = keyPrefix + endKeySuffix;
|
|
468
535
|
|
|
469
|
-
const result = await
|
|
536
|
+
const result = await (
|
|
537
|
+
await this.db
|
|
538
|
+
).query<GraffitiObjectBase>(
|
|
470
539
|
"indexes/objectsPerChannelAndLastModified",
|
|
471
540
|
{ startkey, endkey, include_docs: true },
|
|
472
541
|
);
|
|
@@ -497,7 +566,8 @@ export class GraffitiLocalDatabase
|
|
|
497
566
|
}
|
|
498
567
|
stop();
|
|
499
568
|
return {
|
|
500
|
-
tombstoneRetention:
|
|
569
|
+
tombstoneRetention:
|
|
570
|
+
this.options.tombstoneRetention ?? DEFAULT_TOMBSTONE_RETENTION,
|
|
501
571
|
};
|
|
502
572
|
});
|
|
503
573
|
|
|
@@ -505,8 +575,6 @@ export class GraffitiLocalDatabase
|
|
|
505
575
|
};
|
|
506
576
|
|
|
507
577
|
recoverOrphans: Graffiti["recoverOrphans"] = (schema, session) => {
|
|
508
|
-
const validate = compileGraffitiObjectSchema(this.ajv, schema);
|
|
509
|
-
|
|
510
578
|
const { startKeySuffix, endKeySuffix } =
|
|
511
579
|
this.queryLastModifiedSuffixes(schema);
|
|
512
580
|
const keyPrefix = encodeURIComponent(session.actor) + "/";
|
|
@@ -516,10 +584,15 @@ export class GraffitiLocalDatabase
|
|
|
516
584
|
const repeater: ReturnType<
|
|
517
585
|
typeof Graffiti.prototype.recoverOrphans<typeof schema>
|
|
518
586
|
> = new Repeater(async (push, stop) => {
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
587
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
588
|
+
|
|
589
|
+
const result = await (
|
|
590
|
+
await this.db
|
|
591
|
+
).query<GraffitiObjectBase>("indexes/orphansPerActorAndLastModified", {
|
|
592
|
+
startkey,
|
|
593
|
+
endkey,
|
|
594
|
+
include_docs: true,
|
|
595
|
+
});
|
|
523
596
|
|
|
524
597
|
for (const row of result.rows) {
|
|
525
598
|
const doc = row.doc;
|
|
@@ -535,7 +608,8 @@ export class GraffitiLocalDatabase
|
|
|
535
608
|
}
|
|
536
609
|
stop();
|
|
537
610
|
return {
|
|
538
|
-
tombstoneRetention:
|
|
611
|
+
tombstoneRetention:
|
|
612
|
+
this.options.tombstoneRetention ?? DEFAULT_TOMBSTONE_RETENTION,
|
|
539
613
|
};
|
|
540
614
|
});
|
|
541
615
|
|
|
@@ -546,7 +620,9 @@ export class GraffitiLocalDatabase
|
|
|
546
620
|
const repeater: ReturnType<typeof Graffiti.prototype.channelStats> =
|
|
547
621
|
new Repeater(async (push, stop) => {
|
|
548
622
|
const keyPrefix = encodeURIComponent(session.actor) + "/";
|
|
549
|
-
const result = await
|
|
623
|
+
const result = await (
|
|
624
|
+
await this.db
|
|
625
|
+
).query("indexes/channelStatsPerActor", {
|
|
550
626
|
startkey: keyPrefix,
|
|
551
627
|
endkey: keyPrefix + "\uffff",
|
|
552
628
|
reduce: true,
|