@graffiti-garden/implementation-local 0.5.0 → 0.6.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/dist/browser/index.js +2 -2
- package/dist/browser/index.js.map +3 -3
- package/dist/cjs/database.js +181 -81
- package/dist/cjs/database.js.map +2 -2
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/utilities.js +3 -7
- package/dist/cjs/utilities.js.map +2 -2
- package/dist/database.d.ts +24 -20
- package/dist/database.d.ts.map +1 -1
- package/dist/esm/database.js +182 -83
- package/dist/esm/database.js.map +2 -2
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/utilities.js +3 -7
- package/dist/esm/utilities.js.map +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/utilities.d.ts +2 -3
- package/dist/utilities.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/database.ts +260 -137
- package/src/index.ts +4 -1
- package/src/utilities.ts +3 -15
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/utilities.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n GraffitiErrorInvalidSchema,\n GraffitiErrorInvalidUri,\n GraffitiErrorPatchError,\n GraffitiErrorPatchTestFailed,\n} from \"@graffiti-garden/api\";\nimport type {\n GraffitiObject,\n GraffitiObjectBase,\n GraffitiPatch,\n JSONSchema,\n GraffitiSession,\n
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA
|
|
4
|
+
"sourcesContent": ["import {\n GraffitiErrorInvalidSchema,\n GraffitiErrorInvalidUri,\n GraffitiErrorPatchError,\n GraffitiErrorPatchTestFailed,\n} from \"@graffiti-garden/api\";\nimport type {\n GraffitiObject,\n GraffitiObjectBase,\n GraffitiPatch,\n JSONSchema,\n GraffitiSession,\n GraffitiObjectUrl,\n} from \"@graffiti-garden/api\";\nimport type { Ajv } from \"ajv\";\nimport type { applyPatch } from \"fast-json-patch\";\n\nexport function unpackObjectUrl(url: string | GraffitiObjectUrl) {\n return typeof url === \"string\" ? url : url.url;\n}\n\nexport function randomBase64(numBytes: number = 24) {\n const bytes = new Uint8Array(numBytes);\n crypto.getRandomValues(bytes);\n // Convert it to base64\n const base64 = btoa(String.fromCodePoint(...bytes));\n // Make sure it is url safe\n return base64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/\\=+$/, \"\");\n}\n\nexport function applyGraffitiPatch<Prop extends keyof GraffitiPatch>(\n apply: typeof applyPatch,\n prop: Prop,\n patch: GraffitiPatch,\n object: GraffitiObjectBase,\n): void {\n const ops = patch[prop];\n if (!ops || !ops.length) return;\n try {\n object[prop] = apply(object[prop], ops, true, false).newDocument;\n } catch (e) {\n if (\n typeof e === \"object\" &&\n e &&\n \"name\" in e &&\n typeof e.name === \"string\" &&\n \"message\" in e &&\n typeof e.message === \"string\"\n ) {\n if (e.name === \"TEST_OPERATION_FAILED\") {\n throw new GraffitiErrorPatchTestFailed(e.message);\n } else {\n throw new GraffitiErrorPatchError(e.name + \": \" + e.message);\n }\n } else {\n throw e;\n }\n }\n}\n\nexport function compileGraffitiObjectSchema<Schema extends JSONSchema>(\n ajv: Ajv,\n schema: Schema,\n) {\n try {\n // Force the validation guard because\n // it is too big for the type checker.\n // Fortunately json-schema-to-ts is\n // well tested against ajv.\n return ajv.compile(schema) as (\n data: GraffitiObjectBase,\n ) => data is GraffitiObject<Schema>;\n } catch (error) {\n throw new GraffitiErrorInvalidSchema(\n error instanceof Error ? error.message : undefined,\n );\n }\n}\n\nexport function maskGraffitiObject(\n object: GraffitiObjectBase,\n channels: string[],\n session?: GraffitiSession | null,\n): void {\n if (object.actor !== session?.actor) {\n object.allowed = object.allowed && session ? [session.actor] : undefined;\n object.channels = object.channels.filter((channel) =>\n channels.includes(channel),\n );\n }\n}\nexport function isActorAllowedGraffitiObject(\n object: GraffitiObjectBase,\n session?: GraffitiSession | null,\n) {\n return (\n object.allowed === undefined ||\n object.allowed === null ||\n (!!session?.actor &&\n (object.actor === session.actor ||\n object.allowed.includes(session.actor)))\n );\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAKO;AAYA,SAAS,gBAAgB,KAAiC;AAC/D,SAAO,OAAO,QAAQ,WAAW,MAAM,IAAI;AAC7C;AAEO,SAAS,aAAa,WAAmB,IAAI;AAClD,QAAM,QAAQ,IAAI,WAAW,QAAQ;AACrC,SAAO,gBAAgB,KAAK;AAE5B,QAAM,SAAS,KAAK,OAAO,cAAc,GAAG,KAAK,CAAC;AAElD,SAAO,OAAO,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,QAAQ,EAAE;AAC1E;AAEO,SAAS,mBACd,OACA,MACA,OACA,QACM;AACN,QAAM,MAAM,MAAM,IAAI;AACtB,MAAI,CAAC,OAAO,CAAC,IAAI,OAAQ;AACzB,MAAI;AACF,WAAO,IAAI,IAAI,MAAM,OAAO,IAAI,GAAG,KAAK,MAAM,KAAK,EAAE;AAAA,EACvD,SAAS,GAAG;AACV,QACE,OAAO,MAAM,YACb,KACA,UAAU,KACV,OAAO,EAAE,SAAS,YAClB,aAAa,KACb,OAAO,EAAE,YAAY,UACrB;AACA,UAAI,EAAE,SAAS,yBAAyB;AACtC,cAAM,IAAI,wCAA6B,EAAE,OAAO;AAAA,MAClD,OAAO;AACL,cAAM,IAAI,mCAAwB,EAAE,OAAO,OAAO,EAAE,OAAO;AAAA,MAC7D;AAAA,IACF,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,SAAS,4BACd,KACA,QACA;AACA,MAAI;AAKF,WAAO,IAAI,QAAQ,MAAM;AAAA,EAG3B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,SAAS,mBACd,QACA,UACA,SACM;AACN,MAAI,OAAO,UAAU,SAAS,OAAO;AACnC,WAAO,UAAU,OAAO,WAAW,UAAU,CAAC,QAAQ,KAAK,IAAI;AAC/D,WAAO,WAAW,OAAO,SAAS;AAAA,MAAO,CAAC,YACxC,SAAS,SAAS,OAAO;AAAA,IAC3B;AAAA,EACF;AACF;AACO,SAAS,6BACd,QACA,SACA;AACA,SACE,OAAO,YAAY,UACnB,OAAO,YAAY,QAClB,CAAC,CAAC,SAAS,UACT,OAAO,UAAU,QAAQ,SACxB,OAAO,QAAQ,SAAS,QAAQ,KAAK;AAE7C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/database.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Graffiti, GraffitiObjectBase,
|
|
1
|
+
import type { Graffiti, GraffitiObjectBase, GraffitiObjectUrl, JSONSchema, GraffitiSession, GraffitiObject, GraffitiStream } from "@graffiti-garden/api";
|
|
2
2
|
import type Ajv from "ajv";
|
|
3
3
|
import type { applyPatch } from "fast-json-patch";
|
|
4
4
|
/**
|
|
@@ -15,18 +15,18 @@ export interface GraffitiLocalOptions {
|
|
|
15
15
|
pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;
|
|
16
16
|
/**
|
|
17
17
|
* Includes the scheme and other information (possibly domain name)
|
|
18
|
-
* to prefix prefixes all
|
|
18
|
+
* to prefix prefixes all URLs put in the system. Defaults to `graffiti:local`.
|
|
19
19
|
*/
|
|
20
20
|
origin?: string;
|
|
21
21
|
/**
|
|
22
|
-
* Whether to allow putting objects at arbtirary
|
|
23
|
-
*
|
|
22
|
+
* Whether to allow putting objects at arbtirary URLs, i.e.
|
|
23
|
+
* URLs that are *not* prefixed with the origin or not generated
|
|
24
24
|
* by the system. Defaults to `false`.
|
|
25
25
|
*
|
|
26
26
|
* Allows this implementation to be used as a client-side cache
|
|
27
27
|
* for remote sources.
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
allowSettingArbitraryUrls?: boolean;
|
|
30
30
|
/**
|
|
31
31
|
* Whether to allow the user to set the lastModified field
|
|
32
32
|
* when putting objects. Defaults to `false`.
|
|
@@ -35,34 +35,34 @@ export interface GraffitiLocalOptions {
|
|
|
35
35
|
* for remote sources.
|
|
36
36
|
*/
|
|
37
37
|
allowSettinngLastModified?: boolean;
|
|
38
|
-
/**
|
|
39
|
-
* The time in milliseconds to keep tombstones before deleting them.
|
|
40
|
-
* See the {@link https://api.graffiti.garden/classes/Graffiti.html#discover | `discover` }
|
|
41
|
-
* documentation for more information.
|
|
42
|
-
*/
|
|
43
|
-
tombstoneRetention?: number;
|
|
44
38
|
/**
|
|
45
39
|
* An optional Ajv instance to use for schema validation.
|
|
46
40
|
* If not provided, an internal instance will be created.
|
|
47
41
|
*/
|
|
48
42
|
ajv?: Ajv;
|
|
49
43
|
}
|
|
44
|
+
type GraffitiObjectWithTombstone = GraffitiObjectBase & {
|
|
45
|
+
tombstone: boolean;
|
|
46
|
+
};
|
|
50
47
|
/**
|
|
51
48
|
* An implementation of only the database operations of the
|
|
52
49
|
* GraffitiAPI without synchronization or session management.
|
|
53
50
|
*/
|
|
54
|
-
export declare class GraffitiLocalDatabase implements
|
|
55
|
-
protected db_: Promise<PouchDB.Database<
|
|
51
|
+
export declare class GraffitiLocalDatabase implements Omit<Graffiti, "login" | "logout" | "sessionEvents"> {
|
|
52
|
+
protected db_: Promise<PouchDB.Database<GraffitiObjectWithTombstone>> | undefined;
|
|
56
53
|
protected applyPatch_: Promise<typeof applyPatch> | undefined;
|
|
57
54
|
protected ajv_: Promise<Ajv> | undefined;
|
|
58
55
|
protected readonly options: GraffitiLocalOptions;
|
|
59
56
|
protected readonly origin: string;
|
|
60
|
-
get db(): Promise<PouchDB.Database<
|
|
61
|
-
get applyPatch(): Promise<typeof applyPatch>;
|
|
62
|
-
get ajv(): Promise<Ajv>;
|
|
57
|
+
get db(): Promise<PouchDB.Database<GraffitiObjectWithTombstone>>;
|
|
58
|
+
protected get applyPatch(): Promise<typeof applyPatch>;
|
|
59
|
+
protected get ajv(): Promise<Ajv>;
|
|
60
|
+
protected extractGraffitiObject(object: GraffitiObjectWithTombstone): GraffitiObjectBase;
|
|
63
61
|
constructor(options?: GraffitiLocalOptions);
|
|
64
|
-
protected allDocsAtLocation(
|
|
65
|
-
|
|
62
|
+
protected allDocsAtLocation(objectUrl: string | GraffitiObjectUrl): Promise<PouchDB.Core.ExistingDocument<GraffitiObjectBase & {
|
|
63
|
+
tombstone: boolean;
|
|
64
|
+
} & PouchDB.Core.AllDocsMeta>[]>;
|
|
65
|
+
protected docId(objectUrl: GraffitiObjectUrl): string;
|
|
66
66
|
get: Graffiti["get"];
|
|
67
67
|
/**
|
|
68
68
|
* Deletes all docs at a particular location.
|
|
@@ -72,19 +72,23 @@ export declare class GraffitiLocalDatabase implements Pick<Graffiti, "get" | "pu
|
|
|
72
72
|
* timestamp, the one with the highest `_id` will be
|
|
73
73
|
* spared.
|
|
74
74
|
*/
|
|
75
|
-
protected deleteAtLocation(
|
|
75
|
+
protected deleteAtLocation(url: GraffitiObjectUrl | string, options?: {
|
|
76
76
|
keepLatest?: boolean;
|
|
77
77
|
session?: GraffitiSession;
|
|
78
78
|
}): Promise<GraffitiObjectBase | undefined>;
|
|
79
79
|
delete: Graffiti["delete"];
|
|
80
80
|
put: Graffiti["put"];
|
|
81
81
|
patch: Graffiti["patch"];
|
|
82
|
-
protected queryLastModifiedSuffixes(schema: JSONSchema): {
|
|
82
|
+
protected queryLastModifiedSuffixes(schema: JSONSchema, lastModified?: number): {
|
|
83
83
|
startKeySuffix: string;
|
|
84
84
|
endKeySuffix: string;
|
|
85
85
|
};
|
|
86
|
+
protected discoverMeta<Schema extends JSONSchema>(channels: string[], schema: Schema, session?: GraffitiSession | null, ifModifiedSince?: number): GraffitiStream<GraffitiObject<Schema>>;
|
|
86
87
|
discover: Graffiti["discover"];
|
|
88
|
+
protected recoverOrphansMeta<Schema extends JSONSchema>(schema: Schema, session: GraffitiSession, ifModifiedSince?: number): GraffitiStream<GraffitiObject<Schema>>;
|
|
87
89
|
recoverOrphans: Graffiti["recoverOrphans"];
|
|
88
90
|
channelStats: Graffiti["channelStats"];
|
|
91
|
+
continueStream: Graffiti["continueStream"];
|
|
89
92
|
}
|
|
93
|
+
export {};
|
|
90
94
|
//# sourceMappingURL=database.d.ts.map
|
package/dist/database.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/database.js
CHANGED
|
@@ -9,12 +9,10 @@ import {
|
|
|
9
9
|
applyGraffitiPatch,
|
|
10
10
|
maskGraffitiObject,
|
|
11
11
|
isActorAllowedGraffitiObject,
|
|
12
|
-
isObjectNewer,
|
|
13
12
|
compileGraffitiObjectSchema,
|
|
14
|
-
|
|
13
|
+
unpackObjectUrl
|
|
15
14
|
} from "./utilities.js";
|
|
16
15
|
import { Repeater } from "@repeaterjs/repeater";
|
|
17
|
-
const DEFAULT_TOMBSTONE_RETENTION = 864e5;
|
|
18
16
|
const DEFAULT_ORIGIN = "graffiti:local:";
|
|
19
17
|
class GraffitiLocalDatabase {
|
|
20
18
|
db_;
|
|
@@ -96,6 +94,17 @@ class GraffitiLocalDatabase {
|
|
|
96
94
|
}
|
|
97
95
|
return this.ajv_;
|
|
98
96
|
}
|
|
97
|
+
extractGraffitiObject(object) {
|
|
98
|
+
const { value, channels, allowed, url, actor, lastModified } = object;
|
|
99
|
+
return {
|
|
100
|
+
value,
|
|
101
|
+
channels,
|
|
102
|
+
allowed,
|
|
103
|
+
url,
|
|
104
|
+
actor,
|
|
105
|
+
lastModified
|
|
106
|
+
};
|
|
107
|
+
}
|
|
99
108
|
constructor(options) {
|
|
100
109
|
this.options = options ?? {};
|
|
101
110
|
this.origin = this.options.origin ?? DEFAULT_ORIGIN;
|
|
@@ -103,11 +112,11 @@ class GraffitiLocalDatabase {
|
|
|
103
112
|
this.origin += "/";
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
|
-
async allDocsAtLocation(
|
|
107
|
-
const
|
|
115
|
+
async allDocsAtLocation(objectUrl) {
|
|
116
|
+
const url = unpackObjectUrl(objectUrl) + "/";
|
|
108
117
|
const results = await (await this.db).allDocs({
|
|
109
|
-
startkey:
|
|
110
|
-
endkey:
|
|
118
|
+
startkey: url,
|
|
119
|
+
endkey: url + "\uFFFF",
|
|
111
120
|
// \uffff is the last unicode character
|
|
112
121
|
include_docs: true
|
|
113
122
|
});
|
|
@@ -117,12 +126,12 @@ class GraffitiLocalDatabase {
|
|
|
117
126
|
}, []);
|
|
118
127
|
return docs;
|
|
119
128
|
}
|
|
120
|
-
docId(
|
|
121
|
-
return
|
|
129
|
+
docId(objectUrl) {
|
|
130
|
+
return objectUrl.url + "/" + randomBase64();
|
|
122
131
|
}
|
|
123
132
|
get = async (...args) => {
|
|
124
|
-
const [
|
|
125
|
-
const docsAll = await this.allDocsAtLocation(
|
|
133
|
+
const [urlObject, schema, session] = args;
|
|
134
|
+
const docsAll = await this.allDocsAtLocation(urlObject);
|
|
126
135
|
const docs = docsAll.filter(
|
|
127
136
|
(doc2) => isActorAllowedGraffitiObject(doc2, session)
|
|
128
137
|
);
|
|
@@ -130,8 +139,15 @@ class GraffitiLocalDatabase {
|
|
|
130
139
|
throw new GraffitiErrorNotFound(
|
|
131
140
|
"The object you are trying to get either does not exist or you are not allowed to see it"
|
|
132
141
|
);
|
|
133
|
-
const doc = docs.reduce(
|
|
134
|
-
|
|
142
|
+
const doc = docs.reduce(
|
|
143
|
+
(a, b) => a.lastModified > b.lastModified || a.lastModified === b.lastModified && !a.tombstone && b.tombstone ? a : b
|
|
144
|
+
);
|
|
145
|
+
if (doc.tombstone) {
|
|
146
|
+
throw new GraffitiErrorNotFound(
|
|
147
|
+
"The object you are trying to get either does not exist or you are not allowed to see it"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const object = this.extractGraffitiObject(doc);
|
|
135
151
|
maskGraffitiObject(object, [], session);
|
|
136
152
|
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
137
153
|
if (!validate(object)) {
|
|
@@ -147,10 +163,10 @@ class GraffitiLocalDatabase {
|
|
|
147
163
|
* timestamp, the one with the highest `_id` will be
|
|
148
164
|
* spared.
|
|
149
165
|
*/
|
|
150
|
-
async deleteAtLocation(
|
|
166
|
+
async deleteAtLocation(url, options = {
|
|
151
167
|
keepLatest: false
|
|
152
168
|
}) {
|
|
153
|
-
const docsAtLocationAll = await this.allDocsAtLocation(
|
|
169
|
+
const docsAtLocationAll = await this.allDocsAtLocation(url);
|
|
154
170
|
const docsAtLocationAllowed = options.session ? docsAtLocationAll.filter(
|
|
155
171
|
(doc) => isActorAllowedGraffitiObject(doc, options.session)
|
|
156
172
|
) : docsAtLocationAll;
|
|
@@ -195,10 +211,8 @@ class GraffitiLocalDatabase {
|
|
|
195
211
|
const { id } = resultOrError;
|
|
196
212
|
const deletedDoc = docsToDelete.find((doc) => doc._id === id);
|
|
197
213
|
if (deletedDoc) {
|
|
198
|
-
const { _id, _rev, _conflicts, _attachments, ...object } = deletedDoc;
|
|
199
214
|
deletedObject = {
|
|
200
|
-
...
|
|
201
|
-
tombstone: true,
|
|
215
|
+
...this.extractGraffitiObject(deletedDoc),
|
|
202
216
|
lastModified
|
|
203
217
|
};
|
|
204
218
|
break;
|
|
@@ -208,8 +222,8 @@ class GraffitiLocalDatabase {
|
|
|
208
222
|
return deletedObject;
|
|
209
223
|
}
|
|
210
224
|
delete = async (...args) => {
|
|
211
|
-
const [
|
|
212
|
-
const deletedObject = await this.deleteAtLocation(
|
|
225
|
+
const [url, session] = args;
|
|
226
|
+
const deletedObject = await this.deleteAtLocation(url, {
|
|
213
227
|
session
|
|
214
228
|
});
|
|
215
229
|
if (!deletedObject) {
|
|
@@ -224,13 +238,13 @@ class GraffitiLocalDatabase {
|
|
|
224
238
|
"Cannot put an object with a different actor than the session actor"
|
|
225
239
|
);
|
|
226
240
|
}
|
|
227
|
-
if (objectPartial.
|
|
241
|
+
if (objectPartial.url) {
|
|
228
242
|
let oldObject;
|
|
229
243
|
try {
|
|
230
|
-
oldObject = await this.get(objectPartial.
|
|
244
|
+
oldObject = await this.get(objectPartial.url, {}, session);
|
|
231
245
|
} catch (e) {
|
|
232
246
|
if (e instanceof GraffitiErrorNotFound) {
|
|
233
|
-
if (!this.options.
|
|
247
|
+
if (!this.options.allowSettingArbitraryUrls) {
|
|
234
248
|
throw new GraffitiErrorNotFound(
|
|
235
249
|
"The object you are trying to replace does not exist or you are not allowed to see it"
|
|
236
250
|
);
|
|
@@ -250,7 +264,7 @@ class GraffitiLocalDatabase {
|
|
|
250
264
|
value: objectPartial.value,
|
|
251
265
|
channels: objectPartial.channels,
|
|
252
266
|
allowed: objectPartial.allowed,
|
|
253
|
-
|
|
267
|
+
url: objectPartial.url ?? this.origin + randomBase64(),
|
|
254
268
|
actor: session.actor,
|
|
255
269
|
tombstone: false,
|
|
256
270
|
lastModified
|
|
@@ -275,10 +289,10 @@ class GraffitiLocalDatabase {
|
|
|
275
289
|
}
|
|
276
290
|
};
|
|
277
291
|
patch = async (...args) => {
|
|
278
|
-
const [patch,
|
|
292
|
+
const [patch, url, session] = args;
|
|
279
293
|
let originalObject;
|
|
280
294
|
try {
|
|
281
|
-
originalObject = await this.get(
|
|
295
|
+
originalObject = await this.get(url, {}, session);
|
|
282
296
|
} catch (e) {
|
|
283
297
|
if (e instanceof GraffitiErrorNotFound) {
|
|
284
298
|
throw new GraffitiErrorNotFound(
|
|
@@ -292,10 +306,6 @@ class GraffitiLocalDatabase {
|
|
|
292
306
|
throw new GraffitiErrorForbidden(
|
|
293
307
|
"The object you are trying to patch is owned by another actor"
|
|
294
308
|
);
|
|
295
|
-
} else if (originalObject.tombstone) {
|
|
296
|
-
throw new GraffitiErrorNotFound(
|
|
297
|
-
"The object you are trying to patch has been deleted"
|
|
298
|
-
);
|
|
299
309
|
}
|
|
300
310
|
const patchObject = { ...originalObject };
|
|
301
311
|
for (const prop of ["value", "channels", "allowed"]) {
|
|
@@ -317,6 +327,7 @@ class GraffitiLocalDatabase {
|
|
|
317
327
|
patchObject.lastModified = (/* @__PURE__ */ new Date()).getTime();
|
|
318
328
|
await (await this.db).put({
|
|
319
329
|
...patchObject,
|
|
330
|
+
tombstone: false,
|
|
320
331
|
_id: this.docId(patchObject)
|
|
321
332
|
});
|
|
322
333
|
await this.deleteAtLocation(patchObject, {
|
|
@@ -324,16 +335,15 @@ class GraffitiLocalDatabase {
|
|
|
324
335
|
});
|
|
325
336
|
return {
|
|
326
337
|
...originalObject,
|
|
327
|
-
tombstone: true,
|
|
328
338
|
lastModified: patchObject.lastModified
|
|
329
339
|
};
|
|
330
340
|
};
|
|
331
|
-
queryLastModifiedSuffixes(schema) {
|
|
341
|
+
queryLastModifiedSuffixes(schema, lastModified) {
|
|
332
342
|
let startKeySuffix = "";
|
|
333
343
|
let endKeySuffix = "\uFFFF";
|
|
334
344
|
if (typeof schema === "object" && schema.properties?.lastModified && typeof schema.properties.lastModified === "object") {
|
|
335
345
|
const lastModifiedSchema = schema.properties.lastModified;
|
|
336
|
-
const minimum = lastModifiedSchema.minimum;
|
|
346
|
+
const minimum = lastModified && lastModifiedSchema.minimum ? Math.max(lastModified, lastModifiedSchema.minimum) : lastModified ?? lastModifiedSchema.minimum;
|
|
337
347
|
const exclusiveMinimum = lastModifiedSchema.exclusiveMinimum;
|
|
338
348
|
let intMinimum;
|
|
339
349
|
if (exclusiveMinimum !== void 0) {
|
|
@@ -363,66 +373,115 @@ class GraffitiLocalDatabase {
|
|
|
363
373
|
endKeySuffix
|
|
364
374
|
};
|
|
365
375
|
}
|
|
376
|
+
discoverMeta(channels, schema, session, ifModifiedSince) {
|
|
377
|
+
const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
|
|
378
|
+
schema,
|
|
379
|
+
ifModifiedSince
|
|
380
|
+
);
|
|
381
|
+
const showTombstones = ifModifiedSince !== void 0;
|
|
382
|
+
const repeater = (
|
|
383
|
+
// @ts-ignore
|
|
384
|
+
new Repeater(async (push, stop) => {
|
|
385
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
386
|
+
const processedIds = /* @__PURE__ */ new Set();
|
|
387
|
+
for (const channel of channels) {
|
|
388
|
+
const keyPrefix = encodeURIComponent(channel) + "/";
|
|
389
|
+
const startkey = keyPrefix + startKeySuffix;
|
|
390
|
+
const endkey = keyPrefix + endKeySuffix;
|
|
391
|
+
const result = await (await this.db).query(
|
|
392
|
+
"indexes/objectsPerChannelAndLastModified",
|
|
393
|
+
{ startkey, endkey, include_docs: true }
|
|
394
|
+
);
|
|
395
|
+
for (const row of result.rows) {
|
|
396
|
+
const doc = row.doc;
|
|
397
|
+
if (!doc) continue;
|
|
398
|
+
if (!showTombstones && doc.tombstone) continue;
|
|
399
|
+
const object = this.extractGraffitiObject(doc);
|
|
400
|
+
if (!ifModifiedSince || object.lastModified > ifModifiedSince) {
|
|
401
|
+
ifModifiedSince = object.lastModified;
|
|
402
|
+
}
|
|
403
|
+
if (processedIds.has(doc._id)) continue;
|
|
404
|
+
processedIds.add(doc._id);
|
|
405
|
+
if (!isActorAllowedGraffitiObject(doc, session)) continue;
|
|
406
|
+
maskGraffitiObject(object, channels, session);
|
|
407
|
+
if (validate(object)) {
|
|
408
|
+
await push({
|
|
409
|
+
value: object,
|
|
410
|
+
...doc.tombstone ? { tombstone: true } : {}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
stop();
|
|
416
|
+
const cursor = "discover:" + JSON.stringify({
|
|
417
|
+
channels,
|
|
418
|
+
schema,
|
|
419
|
+
ifModifiedSince,
|
|
420
|
+
actor: session?.actor
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
cursor,
|
|
424
|
+
continue: () => this.continueStream(cursor, session)
|
|
425
|
+
};
|
|
426
|
+
})
|
|
427
|
+
);
|
|
428
|
+
return repeater;
|
|
429
|
+
}
|
|
366
430
|
discover = (...args) => {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
431
|
+
return this.discoverMeta(...args);
|
|
432
|
+
};
|
|
433
|
+
recoverOrphansMeta(schema, session, ifModifiedSince) {
|
|
434
|
+
const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
|
|
435
|
+
schema,
|
|
436
|
+
ifModifiedSince
|
|
437
|
+
);
|
|
438
|
+
const keyPrefix = encodeURIComponent(session.actor) + "/";
|
|
439
|
+
const startkey = keyPrefix + startKeySuffix;
|
|
440
|
+
const endkey = keyPrefix + endKeySuffix;
|
|
441
|
+
const showTombstones = ifModifiedSince !== void 0;
|
|
442
|
+
const repeater = (
|
|
443
|
+
// @ts-ignore
|
|
444
|
+
new Repeater(async (push, stop) => {
|
|
445
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
376
446
|
const result = await (await this.db).query(
|
|
377
|
-
"indexes/
|
|
378
|
-
{
|
|
447
|
+
"indexes/orphansPerActorAndLastModified",
|
|
448
|
+
{
|
|
449
|
+
startkey,
|
|
450
|
+
endkey,
|
|
451
|
+
include_docs: true
|
|
452
|
+
}
|
|
379
453
|
);
|
|
380
454
|
for (const row of result.rows) {
|
|
381
455
|
const doc = row.doc;
|
|
382
456
|
if (!doc) continue;
|
|
383
|
-
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
457
|
+
if (!showTombstones && doc.tombstone) continue;
|
|
458
|
+
if (!ifModifiedSince || doc.lastModified > ifModifiedSince) {
|
|
459
|
+
ifModifiedSince = doc.lastModified;
|
|
460
|
+
}
|
|
461
|
+
const object = this.extractGraffitiObject(doc);
|
|
388
462
|
if (validate(object)) {
|
|
389
|
-
await push({
|
|
463
|
+
await push({
|
|
464
|
+
value: object,
|
|
465
|
+
...doc.tombstone ? { tombstone: true } : {}
|
|
466
|
+
});
|
|
390
467
|
}
|
|
391
468
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const endkey = keyPrefix + endKeySuffix;
|
|
405
|
-
const repeater = new Repeater(async (push, stop) => {
|
|
406
|
-
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
407
|
-
const result = await (await this.db).query("indexes/orphansPerActorAndLastModified", {
|
|
408
|
-
startkey,
|
|
409
|
-
endkey,
|
|
410
|
-
include_docs: true
|
|
411
|
-
});
|
|
412
|
-
for (const row of result.rows) {
|
|
413
|
-
const doc = row.doc;
|
|
414
|
-
if (!doc) continue;
|
|
415
|
-
const { _id, _rev, ...object } = doc;
|
|
416
|
-
if (validate(object)) {
|
|
417
|
-
await push({ value: object });
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
stop();
|
|
421
|
-
return {
|
|
422
|
-
tombstoneRetention: this.options.tombstoneRetention ?? DEFAULT_TOMBSTONE_RETENTION
|
|
423
|
-
};
|
|
424
|
-
});
|
|
469
|
+
stop();
|
|
470
|
+
const cursor = "recover-orphans:" + JSON.stringify({
|
|
471
|
+
schema,
|
|
472
|
+
actor: session.actor,
|
|
473
|
+
ifModifiedSince
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
cursor,
|
|
477
|
+
continue: () => this.continueStream(cursor, session)
|
|
478
|
+
};
|
|
479
|
+
})
|
|
480
|
+
);
|
|
425
481
|
return repeater;
|
|
482
|
+
}
|
|
483
|
+
recoverOrphans = (...args) => {
|
|
484
|
+
return this.recoverOrphansMeta(...args);
|
|
426
485
|
};
|
|
427
486
|
channelStats = (session) => {
|
|
428
487
|
const repeater = new Repeater(async (push, stop) => {
|
|
@@ -448,9 +507,49 @@ class GraffitiLocalDatabase {
|
|
|
448
507
|
});
|
|
449
508
|
}
|
|
450
509
|
stop();
|
|
510
|
+
const cursor = "channel-stats";
|
|
511
|
+
return {
|
|
512
|
+
cursor,
|
|
513
|
+
continue: () => this.continueStream(
|
|
514
|
+
cursor,
|
|
515
|
+
session
|
|
516
|
+
)
|
|
517
|
+
};
|
|
451
518
|
});
|
|
452
519
|
return repeater;
|
|
453
520
|
};
|
|
521
|
+
continueStream = (cursor, session) => {
|
|
522
|
+
if (cursor === "channel-stats") {
|
|
523
|
+
if (!session) {
|
|
524
|
+
throw new GraffitiErrorForbidden(
|
|
525
|
+
"You must be logged in to continue the stream"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return this.channelStats(session);
|
|
529
|
+
} else if (cursor.startsWith("recover-orphans:")) {
|
|
530
|
+
const { schema, actor, ifModifiedSince } = JSON.parse(
|
|
531
|
+
cursor.slice("recover-orphans:".length)
|
|
532
|
+
);
|
|
533
|
+
if (!session || session.actor !== actor) {
|
|
534
|
+
throw new GraffitiErrorForbidden(
|
|
535
|
+
"You must be logged in as the actor same actor who started the stream"
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return this.recoverOrphansMeta(schema, session, ifModifiedSince);
|
|
539
|
+
} else if (cursor.startsWith("discover:")) {
|
|
540
|
+
const { channels, schema, actor, ifModifiedSince } = JSON.parse(
|
|
541
|
+
cursor.slice("discover:".length)
|
|
542
|
+
);
|
|
543
|
+
if (session?.actor !== actor) {
|
|
544
|
+
throw new GraffitiErrorForbidden(
|
|
545
|
+
"You must be logged in as the actor same actor who started the stream"
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return this.discoverMeta(channels, schema, session, ifModifiedSince);
|
|
549
|
+
} else {
|
|
550
|
+
throw new GraffitiErrorNotFound("Cursor not found");
|
|
551
|
+
}
|
|
552
|
+
};
|
|
454
553
|
}
|
|
455
554
|
export {
|
|
456
555
|
GraffitiLocalDatabase
|