@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.
@@ -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 GraffitiLocation,\n} from \"@graffiti-garden/api\";\nimport type { Ajv } from \"ajv\";\nimport type { applyPatch } from \"fast-json-patch\";\n\nexport function unpackLocationOrUri(locationOrUri: GraffitiLocation | string) {\n return typeof locationOrUri === \"string\" ? locationOrUri : locationOrUri.uri;\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 isObjectNewer(\n left: GraffitiObjectBase,\n right: GraffitiObjectBase,\n) {\n return (\n left.lastModified > right.lastModified ||\n (left.lastModified === right.lastModified &&\n !left.tombstone &&\n right.tombstone)\n );\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;AAAA,iBAKO;AAYA,SAAS,oBAAoB,eAA0C;AAC5E,SAAO,OAAO,kBAAkB,WAAW,gBAAgB,cAAc;AAC3E;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,cACd,MACA,OACA;AACA,SACE,KAAK,eAAe,MAAM,gBACzB,KAAK,iBAAiB,MAAM,gBAC3B,CAAC,KAAK,aACN,MAAM;AAEZ;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;",
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
  }
@@ -1,4 +1,4 @@
1
- import type { Graffiti, GraffitiObjectBase, GraffitiLocation, JSONSchema, GraffitiSession } from "@graffiti-garden/api";
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 URIs put in the system. Defaults to `graffiti:local`.
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 URIs, i.e.
23
- * URIs that are *not* prefixed with the origin or not generated
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
- allowSettingArbitraryUris?: boolean;
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 Pick<Graffiti, "get" | "put" | "patch" | "delete" | "discover" | "recoverOrphans" | "channelStats"> {
55
- protected db_: Promise<PouchDB.Database<GraffitiObjectBase>> | undefined;
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<GraffitiObjectBase>>;
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(locationOrUri: GraffitiLocation | string): Promise<PouchDB.Core.ExistingDocument<GraffitiObjectBase & PouchDB.Core.AllDocsMeta>[]>;
65
- protected docId(location: GraffitiLocation): string;
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(locationOrUri: GraffitiLocation | string, options?: {
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
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,eAAe,EAChB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC;IAC7D;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;OAMG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC;CACX;AAKD;;;GAGG;AACH,qBAAa,qBACX,YACE,IAAI,CACF,QAAQ,EACN,KAAK,GACL,KAAK,GACL,OAAO,GACP,QAAQ,GACR,UAAU,GACV,gBAAgB,GAChB,cAAc,CACjB;IAEH,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC,GAAG,SAAS,CAAC;IACzE,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,UAAU,CAAC,GAAG,SAAS,CAAC;IAC9D,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACzC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACjD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAElC,IAAI,EAAE,kDA+EL;IAED,IAAI,UAAU,+BAQb;IAED,IAAI,GAAG,iBAUN;gBAEW,OAAO,CAAC,EAAE,oBAAoB;cAQ1B,iBAAiB,CAAC,aAAa,EAAE,gBAAgB,GAAG,MAAM;IAuB1E,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,gBAAgB;IAI1C,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CA6BlB;IAEF;;;;;;;OAOG;cACa,gBAAgB,CAC9B,aAAa,EAAE,gBAAgB,GAAG,MAAM,EACxC,OAAO,GAAE;QACP,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,OAAO,CAAC,EAAE,eAAe,CAAC;KAG3B;IAsFH,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CASxB;IAEF,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAmElB;IAEF,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CA8EtB;IAEF,SAAS,CAAC,yBAAyB,CAAC,MAAM,EAAE,UAAU;;;;IA+CtD,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAyD5B;IAEF,cAAc,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAwCxC;IAEF,YAAY,EAAE,QAAQ,CAAC,cAAc,CAAC,CA8BpC;CACH"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"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"}
@@ -9,12 +9,10 @@ import {
9
9
  applyGraffitiPatch,
10
10
  maskGraffitiObject,
11
11
  isActorAllowedGraffitiObject,
12
- isObjectNewer,
13
12
  compileGraffitiObjectSchema,
14
- unpackLocationOrUri
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(locationOrUri) {
107
- const uri = unpackLocationOrUri(locationOrUri) + "/";
115
+ async allDocsAtLocation(objectUrl) {
116
+ const url = unpackObjectUrl(objectUrl) + "/";
108
117
  const results = await (await this.db).allDocs({
109
- startkey: uri,
110
- endkey: uri + "\uFFFF",
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(location) {
121
- return location.uri + "/" + randomBase64();
129
+ docId(objectUrl) {
130
+ return objectUrl.url + "/" + randomBase64();
122
131
  }
123
132
  get = async (...args) => {
124
- const [locationOrUri, schema, session] = args;
125
- const docsAll = await this.allDocsAtLocation(locationOrUri);
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((a, b) => isObjectNewer(a, b) ? a : b);
134
- const { _id, _rev, _conflicts, _attachments, ...object } = doc;
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(locationOrUri, options = {
166
+ async deleteAtLocation(url, options = {
151
167
  keepLatest: false
152
168
  }) {
153
- const docsAtLocationAll = await this.allDocsAtLocation(locationOrUri);
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
- ...object,
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 [locationOrUri, session] = args;
212
- const deletedObject = await this.deleteAtLocation(locationOrUri, {
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.uri) {
241
+ if (objectPartial.url) {
228
242
  let oldObject;
229
243
  try {
230
- oldObject = await this.get(objectPartial.uri, {}, session);
244
+ oldObject = await this.get(objectPartial.url, {}, session);
231
245
  } catch (e) {
232
246
  if (e instanceof GraffitiErrorNotFound) {
233
- if (!this.options.allowSettingArbitraryUris) {
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
- uri: objectPartial.uri ?? this.origin + randomBase64(),
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, locationOrUri, session] = args;
292
+ const [patch, url, session] = args;
279
293
  let originalObject;
280
294
  try {
281
- originalObject = await this.get(locationOrUri, {}, session);
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
- const [channels, schema, session] = args;
368
- const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(schema);
369
- const repeater = new Repeater(async (push, stop) => {
370
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
371
- const processedIds = /* @__PURE__ */ new Set();
372
- for (const channel of channels) {
373
- const keyPrefix = encodeURIComponent(channel) + "/";
374
- const startkey = keyPrefix + startKeySuffix;
375
- const endkey = keyPrefix + endKeySuffix;
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/objectsPerChannelAndLastModified",
378
- { startkey, endkey, include_docs: true }
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
- const { _id, _rev, ...object } = doc;
384
- if (processedIds.has(_id)) continue;
385
- processedIds.add(_id);
386
- if (!isActorAllowedGraffitiObject(doc, session)) continue;
387
- maskGraffitiObject(object, channels, session);
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({ value: object });
463
+ await push({
464
+ value: object,
465
+ ...doc.tombstone ? { tombstone: true } : {}
466
+ });
390
467
  }
391
468
  }
392
- }
393
- stop();
394
- return {
395
- tombstoneRetention: this.options.tombstoneRetention ?? DEFAULT_TOMBSTONE_RETENTION
396
- };
397
- });
398
- return repeater;
399
- };
400
- recoverOrphans = (schema, session) => {
401
- const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(schema);
402
- const keyPrefix = encodeURIComponent(session.actor) + "/";
403
- const startkey = keyPrefix + startKeySuffix;
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