@bojanrajkovic/mcp-paprika 1.3.0 → 1.4.0-beta.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 +2 -2
- package/dist/auth/types.d.ts +24 -24
- package/dist/cache/aisle-store.d.ts +9 -0
- package/dist/cache/aisle-store.js +17 -0
- package/dist/cache/disk/root.d.ts +5 -1
- package/dist/cache/disk/root.js +40 -2
- package/dist/cache/grocery-ingredient-store.d.ts +18 -0
- package/dist/cache/grocery-ingredient-store.js +33 -0
- package/dist/cache/grocery-item-store.d.ts +16 -0
- package/dist/cache/grocery-item-store.js +29 -0
- package/dist/cache/grocery-list-store.d.ts +16 -0
- package/dist/cache/grocery-list-store.js +38 -0
- package/dist/cache/pantry-store.d.ts +2 -12
- package/dist/cache/pantry-store.js +2 -45
- package/dist/cache/recipe-store.d.ts +3 -0
- package/dist/cache/recipe-store.js +7 -0
- package/dist/entity/index.d.ts +1 -0
- package/dist/entity/index.js +1 -0
- package/dist/entity/tombstone-store.d.ts +10 -0
- package/dist/entity/tombstone-store.js +21 -0
- package/dist/paprika/client.d.ts +12 -13
- package/dist/paprika/client.js +83 -34
- package/dist/paprika/sync.d.ts +18 -1
- package/dist/paprika/sync.js +136 -57
- package/dist/paprika/types.d.ts +275 -13
- package/dist/paprika/types.js +109 -4
- package/dist/resources/grocery-lists.d.ts +3 -0
- package/dist/resources/grocery-lists.js +39 -0
- package/dist/resources/recipes.js +10 -1
- package/dist/server/app-context.d.ts +8 -0
- package/dist/server/build.d.ts +1 -1
- package/dist/server/build.js +67 -6
- package/dist/tools/aisle-helpers.d.ts +19 -0
- package/dist/tools/aisle-helpers.js +70 -0
- package/dist/{resources/pantry.d.ts → tools/aisles.d.ts} +1 -1
- package/dist/tools/aisles.js +24 -0
- package/dist/tools/categories.js +25 -4
- package/dist/tools/discover.js +4 -9
- package/dist/tools/filter.js +3 -9
- package/dist/tools/grocery-clear.d.ts +4 -0
- package/dist/tools/grocery-clear.js +71 -0
- package/dist/tools/grocery-helpers.d.ts +51 -0
- package/dist/tools/grocery-helpers.js +193 -0
- package/dist/tools/grocery-item.d.ts +5 -0
- package/dist/tools/grocery-item.js +223 -0
- package/dist/tools/grocery-list.d.ts +7 -0
- package/dist/tools/grocery-list.js +188 -0
- package/dist/tools/{pantry-add.d.ts → grocery-move.d.ts} +1 -1
- package/dist/tools/grocery-move.js +95 -0
- package/dist/tools/helpers.d.ts +1 -0
- package/dist/tools/helpers.js +23 -0
- package/dist/tools/pantry-batch-add.d.ts +3 -0
- package/dist/tools/pantry-batch-add.js +151 -0
- package/dist/tools/pantry-delete.js +2 -2
- package/dist/tools/pantry-helpers.d.ts +12 -5
- package/dist/tools/pantry-helpers.js +58 -7
- package/dist/tools/pantry-list.js +3 -1
- package/dist/tools/pantry-update.js +37 -13
- package/dist/tools/search.js +3 -18
- package/package.json +9 -8
- package/dist/resources/pantry.js +0 -32
- package/dist/tools/pantry-add.js +0 -76
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@ An [MCP](https://modelcontextprotocol.io/) server for [Paprika](https://www.papr
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **26 tools** for recipe, pantry, and grocery management — search, filter, CRUD, categories, pagination, pantry inventory, aisles, grocery list management, grocery item management
|
|
8
8
|
- **Semantic search** via `discover_recipes` — find recipes by natural language description using any OpenAI-compatible embedding provider
|
|
9
9
|
- **Background sync** — keeps your local cache in sync with Paprika's cloud
|
|
10
|
-
- **MCP resources** — expose recipes as `paprika://recipe/{uid}` and
|
|
10
|
+
- **MCP resources** — expose recipes as `paprika://recipe/{uid}` and grocery lists as `paprika://grocery-list/{uid}` resources
|
|
11
11
|
- **Two transports** — stdio (default, for CLI clients) and Streamable HTTP (for mobile/web clients)
|
|
12
12
|
- **Container image** — `Dockerfile` ships a distroless runtime ready for self-hosting
|
|
13
13
|
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -240,13 +240,13 @@ export declare const IdentitySchema: z.ZodObject<{
|
|
|
240
240
|
sub: z.ZodString;
|
|
241
241
|
source: z.ZodEnum<["email", "sub"]>;
|
|
242
242
|
}, "strip", z.ZodTypeAny, {
|
|
243
|
-
|
|
243
|
+
source: "sub" | "email";
|
|
244
244
|
sub: string;
|
|
245
|
-
source: "email" | "sub";
|
|
246
|
-
}, {
|
|
247
245
|
email: string | null;
|
|
246
|
+
}, {
|
|
247
|
+
source: "sub" | "email";
|
|
248
248
|
sub: string;
|
|
249
|
-
|
|
249
|
+
email: string | null;
|
|
250
250
|
}>;
|
|
251
251
|
/**
|
|
252
252
|
* RFC 8707 resource indicator — must be a fully-qualified URL or an explicit empty
|
|
@@ -264,13 +264,13 @@ export declare const OAuthTokenSchema: z.ZodObject<{
|
|
|
264
264
|
sub: z.ZodString;
|
|
265
265
|
source: z.ZodEnum<["email", "sub"]>;
|
|
266
266
|
}, "strip", z.ZodTypeAny, {
|
|
267
|
-
|
|
267
|
+
source: "sub" | "email";
|
|
268
268
|
sub: string;
|
|
269
|
-
source: "email" | "sub";
|
|
270
|
-
}, {
|
|
271
269
|
email: string | null;
|
|
270
|
+
}, {
|
|
271
|
+
source: "sub" | "email";
|
|
272
272
|
sub: string;
|
|
273
|
-
|
|
273
|
+
email: string | null;
|
|
274
274
|
}>;
|
|
275
275
|
resource: z.ZodUnion<[z.ZodString, z.ZodLiteral<"">]>;
|
|
276
276
|
expiresAt: z.ZodNumber;
|
|
@@ -282,9 +282,9 @@ export declare const OAuthTokenSchema: z.ZodObject<{
|
|
|
282
282
|
scope: string;
|
|
283
283
|
createdAt: number;
|
|
284
284
|
identity: {
|
|
285
|
-
|
|
285
|
+
source: "sub" | "email";
|
|
286
286
|
sub: string;
|
|
287
|
-
|
|
287
|
+
email: string | null;
|
|
288
288
|
};
|
|
289
289
|
tokenHash: string;
|
|
290
290
|
kind: "access" | "refresh";
|
|
@@ -296,9 +296,9 @@ export declare const OAuthTokenSchema: z.ZodObject<{
|
|
|
296
296
|
scope: string;
|
|
297
297
|
createdAt: number;
|
|
298
298
|
identity: {
|
|
299
|
-
|
|
299
|
+
source: "sub" | "email";
|
|
300
300
|
sub: string;
|
|
301
|
-
|
|
301
|
+
email: string | null;
|
|
302
302
|
};
|
|
303
303
|
tokenHash: string;
|
|
304
304
|
kind: "access" | "refresh";
|
|
@@ -353,13 +353,13 @@ export declare const AuthCodeStateSchema: z.ZodObject<{
|
|
|
353
353
|
sub: z.ZodString;
|
|
354
354
|
source: z.ZodEnum<["email", "sub"]>;
|
|
355
355
|
}, "strip", z.ZodTypeAny, {
|
|
356
|
-
|
|
356
|
+
source: "sub" | "email";
|
|
357
357
|
sub: string;
|
|
358
|
-
source: "email" | "sub";
|
|
359
|
-
}, {
|
|
360
358
|
email: string | null;
|
|
359
|
+
}, {
|
|
360
|
+
source: "sub" | "email";
|
|
361
361
|
sub: string;
|
|
362
|
-
|
|
362
|
+
email: string | null;
|
|
363
363
|
}>;
|
|
364
364
|
}, "strip", z.ZodTypeAny, {
|
|
365
365
|
clientId: string;
|
|
@@ -370,9 +370,9 @@ export declare const AuthCodeStateSchema: z.ZodObject<{
|
|
|
370
370
|
scope: string;
|
|
371
371
|
createdAt: number;
|
|
372
372
|
identity: {
|
|
373
|
-
|
|
373
|
+
source: "sub" | "email";
|
|
374
374
|
sub: string;
|
|
375
|
-
|
|
375
|
+
email: string | null;
|
|
376
376
|
};
|
|
377
377
|
}, {
|
|
378
378
|
clientId: string;
|
|
@@ -383,9 +383,9 @@ export declare const AuthCodeStateSchema: z.ZodObject<{
|
|
|
383
383
|
scope: string;
|
|
384
384
|
createdAt: number;
|
|
385
385
|
identity: {
|
|
386
|
-
|
|
386
|
+
source: "sub" | "email";
|
|
387
387
|
sub: string;
|
|
388
|
-
|
|
388
|
+
email: string | null;
|
|
389
389
|
};
|
|
390
390
|
}>;
|
|
391
391
|
export type AuthCodeState = z.infer<typeof AuthCodeStateSchema>;
|
|
@@ -423,13 +423,13 @@ export declare const AuthInfoExtraSchema: z.ZodObject<{
|
|
|
423
423
|
sub: z.ZodString;
|
|
424
424
|
source: z.ZodEnum<["email", "sub"]>;
|
|
425
425
|
}, "strip", z.ZodTypeAny, {
|
|
426
|
-
|
|
426
|
+
source: "sub" | "email";
|
|
427
427
|
sub: string;
|
|
428
|
-
source: "email" | "sub";
|
|
429
|
-
}, {
|
|
430
428
|
email: string | null;
|
|
429
|
+
}, {
|
|
430
|
+
source: "sub" | "email";
|
|
431
431
|
sub: string;
|
|
432
|
-
|
|
432
|
+
email: string | null;
|
|
433
433
|
}>;
|
|
434
434
|
export type AuthInfoExtra = z.infer<typeof AuthInfoExtraSchema>;
|
|
435
435
|
export interface AuthContext {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { EntityStore } from "../entity/index.js";
|
|
2
|
+
import type { Aisle, AisleUid } from "../paprika/types.js";
|
|
3
|
+
export declare class AisleStore extends EntityStore<Aisle, AisleUid> {
|
|
4
|
+
constructor(opts?: {
|
|
5
|
+
readonly pendingWriteTtlMs?: number;
|
|
6
|
+
});
|
|
7
|
+
load(items: ReadonlyArray<Aisle>): void;
|
|
8
|
+
resolveByName(name: string): Aisle | undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EntityStore } from "../entity/index.js";
|
|
2
|
+
export class AisleStore extends EntityStore {
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
super(opts ?? {});
|
|
5
|
+
}
|
|
6
|
+
load(items) {
|
|
7
|
+
this.baseLoad(items);
|
|
8
|
+
}
|
|
9
|
+
resolveByName(name) {
|
|
10
|
+
const needle = name.toLowerCase();
|
|
11
|
+
for (const aisle of this._items.values()) {
|
|
12
|
+
if (aisle.name.toLowerCase() === needle)
|
|
13
|
+
return aisle;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
2
|
import type { OAuthToken } from "../../auth/types.js";
|
|
3
|
-
import type { Category, PantryItem } from "../../paprika/types.js";
|
|
3
|
+
import type { Aisle, Category, GroceryIngredient, GroceryItem, GroceryList, PantryItem } from "../../paprika/types.js";
|
|
4
4
|
import { DiskCache } from "./base.js";
|
|
5
5
|
import { OAuthClientDiskCache } from "./oauth-clients.js";
|
|
6
6
|
import { RecipeDiskCache } from "./recipes.js";
|
|
@@ -18,8 +18,12 @@ export declare class DiskCacheRoot {
|
|
|
18
18
|
readonly recipes: RecipeDiskCache;
|
|
19
19
|
readonly categories: DiskCache<Category>;
|
|
20
20
|
readonly pantry: DiskCache<PantryItem>;
|
|
21
|
+
readonly aisles: DiskCache<Aisle>;
|
|
21
22
|
readonly oauthClients: OAuthClientDiskCache;
|
|
22
23
|
readonly oauthTokens: DiskCache<OAuthToken>;
|
|
24
|
+
readonly groceryLists: DiskCache<GroceryList>;
|
|
25
|
+
readonly groceryItems: DiskCache<GroceryItem>;
|
|
26
|
+
readonly groceryIngredients: DiskCache<GroceryIngredient>;
|
|
23
27
|
private readonly _cacheDir;
|
|
24
28
|
private readonly _subcaches;
|
|
25
29
|
private readonly log;
|
package/dist/cache/disk/root.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, rename, unlink } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { OAuthTokenSchema } from "../../auth/types.js";
|
|
5
|
-
import { CategoryStoredSchema, PantryItemStoredSchema } from "../../paprika/types.js";
|
|
5
|
+
import { AisleStoredSchema, CategoryStoredSchema, GroceryIngredientStoredSchema, GroceryItemStoredSchema, GroceryListStoredSchema, PantryItemStoredSchema, } from "../../paprika/types.js";
|
|
6
6
|
import { isNodeError } from "../../utils/errors.js";
|
|
7
7
|
import { SILENT_LOG } from "../../utils/log.js";
|
|
8
8
|
import { DiskCache, writeFileAtomic } from "./base.js";
|
|
@@ -27,8 +27,12 @@ export class DiskCacheRoot {
|
|
|
27
27
|
recipes;
|
|
28
28
|
categories;
|
|
29
29
|
pantry;
|
|
30
|
+
aisles;
|
|
30
31
|
oauthClients;
|
|
31
32
|
oauthTokens;
|
|
33
|
+
groceryLists;
|
|
34
|
+
groceryItems;
|
|
35
|
+
groceryIngredients;
|
|
32
36
|
_cacheDir;
|
|
33
37
|
_subcaches;
|
|
34
38
|
log;
|
|
@@ -49,6 +53,12 @@ export class DiskCacheRoot {
|
|
|
49
53
|
getKey: (i) => i.uid,
|
|
50
54
|
...logOpts,
|
|
51
55
|
});
|
|
56
|
+
this.aisles = new DiskCache({
|
|
57
|
+
subdir: join(cacheDir, "aisles"),
|
|
58
|
+
parse: (raw) => AisleStoredSchema.parse(raw),
|
|
59
|
+
getKey: (a) => a.uid,
|
|
60
|
+
...logOpts,
|
|
61
|
+
});
|
|
52
62
|
this.oauthClients = new OAuthClientDiskCache({ subdir: join(cacheDir, "oauthClients"), ...logOpts });
|
|
53
63
|
this.oauthTokens = new DiskCache({
|
|
54
64
|
subdir: join(cacheDir, "oauthTokens"),
|
|
@@ -56,7 +66,35 @@ export class DiskCacheRoot {
|
|
|
56
66
|
getKey: (t) => t.tokenHash,
|
|
57
67
|
...logOpts,
|
|
58
68
|
});
|
|
59
|
-
this.
|
|
69
|
+
this.groceryLists = new DiskCache({
|
|
70
|
+
subdir: join(cacheDir, "grocerylists"),
|
|
71
|
+
parse: (raw) => GroceryListStoredSchema.parse(raw),
|
|
72
|
+
getKey: (l) => l.uid,
|
|
73
|
+
...logOpts,
|
|
74
|
+
});
|
|
75
|
+
this.groceryItems = new DiskCache({
|
|
76
|
+
subdir: join(cacheDir, "groceryitems"),
|
|
77
|
+
parse: (raw) => GroceryItemStoredSchema.parse(raw),
|
|
78
|
+
getKey: (i) => i.uid,
|
|
79
|
+
...logOpts,
|
|
80
|
+
});
|
|
81
|
+
this.groceryIngredients = new DiskCache({
|
|
82
|
+
subdir: join(cacheDir, "groceryingredients"),
|
|
83
|
+
parse: (raw) => GroceryIngredientStoredSchema.parse(raw),
|
|
84
|
+
getKey: (i) => i.uid,
|
|
85
|
+
...logOpts,
|
|
86
|
+
});
|
|
87
|
+
this._subcaches = [
|
|
88
|
+
this.recipes,
|
|
89
|
+
this.categories,
|
|
90
|
+
this.pantry,
|
|
91
|
+
this.aisles,
|
|
92
|
+
this.oauthClients,
|
|
93
|
+
this.oauthTokens,
|
|
94
|
+
this.groceryLists,
|
|
95
|
+
this.groceryItems,
|
|
96
|
+
this.groceryIngredients,
|
|
97
|
+
];
|
|
60
98
|
}
|
|
61
99
|
async init() {
|
|
62
100
|
await this._maybeMigrateLegacyIndex();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GroceryIngredient } from "../paprika/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory store for grocery ingredients, keyed by lowercase name for
|
|
4
|
+
* case-insensitive lookup. Unlike GroceryListStore and GroceryItemStore,
|
|
5
|
+
* this is a plain class — not an EntityStore subclass — because ingredients
|
|
6
|
+
* have no pending-writes, no tombstones, and no sweepPending. Internal
|
|
7
|
+
* storage is a Map<string, GroceryIngredient> keyed by lowercase name.
|
|
8
|
+
*/
|
|
9
|
+
export declare class GroceryIngredientStore {
|
|
10
|
+
private readonly _items;
|
|
11
|
+
private _hasSynced;
|
|
12
|
+
get hasSynced(): boolean;
|
|
13
|
+
get size(): number;
|
|
14
|
+
load(items: ReadonlyArray<GroceryIngredient>): void;
|
|
15
|
+
set(ingredient: GroceryIngredient): void;
|
|
16
|
+
lookupByName(name: string): GroceryIngredient | undefined;
|
|
17
|
+
getAll(): Array<GroceryIngredient>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory store for grocery ingredients, keyed by lowercase name for
|
|
3
|
+
* case-insensitive lookup. Unlike GroceryListStore and GroceryItemStore,
|
|
4
|
+
* this is a plain class — not an EntityStore subclass — because ingredients
|
|
5
|
+
* have no pending-writes, no tombstones, and no sweepPending. Internal
|
|
6
|
+
* storage is a Map<string, GroceryIngredient> keyed by lowercase name.
|
|
7
|
+
*/
|
|
8
|
+
export class GroceryIngredientStore {
|
|
9
|
+
_items = new Map();
|
|
10
|
+
_hasSynced = false;
|
|
11
|
+
get hasSynced() {
|
|
12
|
+
return this._hasSynced;
|
|
13
|
+
}
|
|
14
|
+
get size() {
|
|
15
|
+
return this._items.size;
|
|
16
|
+
}
|
|
17
|
+
load(items) {
|
|
18
|
+
this._items.clear();
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
this._items.set(item.name.toLowerCase(), item);
|
|
21
|
+
}
|
|
22
|
+
this._hasSynced = true;
|
|
23
|
+
}
|
|
24
|
+
set(ingredient) {
|
|
25
|
+
this._items.set(ingredient.name.toLowerCase(), ingredient);
|
|
26
|
+
}
|
|
27
|
+
lookupByName(name) {
|
|
28
|
+
return this._items.get(name.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
getAll() {
|
|
31
|
+
return [...this._items.values()];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TombstoneEntityStore } from "../entity/index.js";
|
|
2
|
+
import type { GroceryItem, GroceryItemUid } from "../paprika/types.js";
|
|
3
|
+
export declare class GroceryItemStore extends TombstoneEntityStore<GroceryItem, GroceryItemUid> {
|
|
4
|
+
constructor(opts?: {
|
|
5
|
+
readonly pendingWriteTtlMs?: number;
|
|
6
|
+
});
|
|
7
|
+
/**
|
|
8
|
+
* Returns all non-tombstoned items whose listUid matches the given value.
|
|
9
|
+
*/
|
|
10
|
+
getByListUid(listUid: string): Array<GroceryItem>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns all non-tombstoned items in the given list that have been
|
|
13
|
+
* marked as purchased.
|
|
14
|
+
*/
|
|
15
|
+
getPurchasedByList(listUid: string): Array<GroceryItem>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { TombstoneEntityStore } from "../entity/index.js";
|
|
2
|
+
export class GroceryItemStore extends TombstoneEntityStore {
|
|
3
|
+
constructor(opts) {
|
|
4
|
+
super(opts ?? {});
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns all non-tombstoned items whose listUid matches the given value.
|
|
8
|
+
*/
|
|
9
|
+
getByListUid(listUid) {
|
|
10
|
+
const result = [];
|
|
11
|
+
for (const item of this._items.values()) {
|
|
12
|
+
if (item.listUid === listUid)
|
|
13
|
+
result.push(item);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns all non-tombstoned items in the given list that have been
|
|
19
|
+
* marked as purchased.
|
|
20
|
+
*/
|
|
21
|
+
getPurchasedByList(listUid) {
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const item of this._items.values()) {
|
|
24
|
+
if (item.listUid === listUid && item.purchased)
|
|
25
|
+
result.push(item);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TombstoneEntityStore } from "../entity/index.js";
|
|
2
|
+
import type { GroceryList, GroceryListUid } from "../paprika/types.js";
|
|
3
|
+
export declare class GroceryListStore extends TombstoneEntityStore<GroceryList, GroceryListUid> {
|
|
4
|
+
private _lastSyncedAt;
|
|
5
|
+
constructor(opts?: {
|
|
6
|
+
readonly pendingWriteTtlMs?: number;
|
|
7
|
+
});
|
|
8
|
+
get lastSyncedAt(): Date | null;
|
|
9
|
+
setLastSyncedAt(at?: Date): void;
|
|
10
|
+
/**
|
|
11
|
+
* Tiered case-insensitive name lookup: exact > starts-with > contains.
|
|
12
|
+
* Returns items from at most one tier. Excludes deleted items (they are
|
|
13
|
+
* removed from _items by delete() before this is called).
|
|
14
|
+
*/
|
|
15
|
+
findByName(query: string): Array<GroceryList>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { TombstoneEntityStore } from "../entity/index.js";
|
|
2
|
+
export class GroceryListStore extends TombstoneEntityStore {
|
|
3
|
+
_lastSyncedAt = null;
|
|
4
|
+
constructor(opts) {
|
|
5
|
+
super(opts ?? {});
|
|
6
|
+
}
|
|
7
|
+
get lastSyncedAt() {
|
|
8
|
+
return this._lastSyncedAt;
|
|
9
|
+
}
|
|
10
|
+
setLastSyncedAt(at = new Date()) {
|
|
11
|
+
this._lastSyncedAt = at;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Tiered case-insensitive name lookup: exact > starts-with > contains.
|
|
15
|
+
* Returns items from at most one tier. Excludes deleted items (they are
|
|
16
|
+
* removed from _items by delete() before this is called).
|
|
17
|
+
*/
|
|
18
|
+
findByName(query) {
|
|
19
|
+
const needle = query.toLowerCase();
|
|
20
|
+
const exact = [];
|
|
21
|
+
const startsWith = [];
|
|
22
|
+
const contains = [];
|
|
23
|
+
for (const list of this._items.values()) {
|
|
24
|
+
const name = list.name.toLowerCase();
|
|
25
|
+
if (name === needle)
|
|
26
|
+
exact.push(list);
|
|
27
|
+
else if (name.startsWith(needle))
|
|
28
|
+
startsWith.push(list);
|
|
29
|
+
else if (name.includes(needle))
|
|
30
|
+
contains.push(list);
|
|
31
|
+
}
|
|
32
|
+
if (exact.length > 0)
|
|
33
|
+
return exact;
|
|
34
|
+
if (startsWith.length > 0)
|
|
35
|
+
return startsWith;
|
|
36
|
+
return contains;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TombstoneEntityStore } from "../entity/index.js";
|
|
2
2
|
import type { PantryItem, PantryItemUid } from "../paprika/types.js";
|
|
3
|
-
export declare class PantryStore extends
|
|
4
|
-
private readonly _tombstones;
|
|
3
|
+
export declare class PantryStore extends TombstoneEntityStore<PantryItem, PantryItemUid> {
|
|
5
4
|
constructor(opts?: {
|
|
6
5
|
readonly pendingWriteTtlMs?: number;
|
|
7
6
|
});
|
|
8
|
-
load(items: ReadonlyArray<PantryItem>): void;
|
|
9
|
-
set(item: PantryItem): void;
|
|
10
|
-
delete(uid: PantryItemUid): void;
|
|
11
|
-
/**
|
|
12
|
-
* Returns true if `uid` was soft-deleted via this store in the current
|
|
13
|
-
* session (since the last `load()`). Used by `delete_pantry_item` to give
|
|
14
|
-
* idempotent retried-delete callers a clear "already deleted" signal.
|
|
15
|
-
*/
|
|
16
|
-
isTombstone(uid: PantryItemUid): boolean;
|
|
17
7
|
findByIngredient(query: string): Array<PantryItem>;
|
|
18
8
|
}
|
|
@@ -1,51 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export class PantryStore extends
|
|
3
|
-
// Tombstones track UIDs that were soft-deleted via this client, so
|
|
4
|
-
// `delete_pantry_item` can return a distinct "already deleted" message for
|
|
5
|
-
// retried calls (server upserts by UID and the live-items map alone can't
|
|
6
|
-
// distinguish "I deleted this" from "this never existed"). Tombstones
|
|
7
|
-
// persist across `load()` so delayed retries that span a sync cycle still
|
|
8
|
-
// get the idempotent signal — `load()` only un-tombstones UIDs that are
|
|
9
|
-
// now back in the live items list (i.e. resurrected by the server, e.g.,
|
|
10
|
-
// un-deleted via another client). The tombstone set therefore stays
|
|
11
|
-
// disjoint from `_items` after every load.
|
|
12
|
-
_tombstones = new Set();
|
|
1
|
+
import { TombstoneEntityStore } from "../entity/index.js";
|
|
2
|
+
export class PantryStore extends TombstoneEntityStore {
|
|
13
3
|
constructor(opts) {
|
|
14
4
|
super(opts ?? {});
|
|
15
5
|
}
|
|
16
|
-
load(items) {
|
|
17
|
-
this.baseLoad(items);
|
|
18
|
-
// Un-tombstone any UID that came back in the snapshot (resurrection: item
|
|
19
|
-
// returned from the server supersedes the local soft-delete).
|
|
20
|
-
for (const item of items) {
|
|
21
|
-
this._tombstones.delete(item.uid);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
set(item) {
|
|
25
|
-
super.set(item);
|
|
26
|
-
this._tombstones.delete(item.uid);
|
|
27
|
-
}
|
|
28
|
-
delete(uid) {
|
|
29
|
-
// Always tombstone, regardless of whether `uid` is currently in `_items`.
|
|
30
|
-
// The only caller is `commitPantryItem`'s delete branch (post-successful
|
|
31
|
-
// savePantryItem), but several awaits separate the save from the local
|
|
32
|
-
// commit; SyncEngine.syncOnce() can interleave a `load(...)` that wipes
|
|
33
|
-
// the UID from `_items` before commit lands. Conditioning the tombstone
|
|
34
|
-
// on `_items.has(uid)` would silently drop the idempotent retry signal
|
|
35
|
-
// in exactly that race. Spurious tombstones from other callers are
|
|
36
|
-
// acceptable: an extra "already deleted" message is harmless; a missing
|
|
37
|
-
// one isn't.
|
|
38
|
-
this._tombstones.add(uid);
|
|
39
|
-
super.delete(uid);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Returns true if `uid` was soft-deleted via this store in the current
|
|
43
|
-
* session (since the last `load()`). Used by `delete_pantry_item` to give
|
|
44
|
-
* idempotent retried-delete callers a clear "already deleted" signal.
|
|
45
|
-
*/
|
|
46
|
-
isTombstone(uid) {
|
|
47
|
-
return this._tombstones.has(uid);
|
|
48
|
-
}
|
|
49
6
|
findByIngredient(query) {
|
|
50
7
|
const needle = query.toLowerCase();
|
|
51
8
|
const exact = [];
|
|
@@ -16,9 +16,12 @@ export type TimeConstraints = {
|
|
|
16
16
|
};
|
|
17
17
|
export declare class RecipeStore extends EntityStore<Recipe, RecipeUid> {
|
|
18
18
|
private readonly categories;
|
|
19
|
+
private _lastSyncedAt;
|
|
19
20
|
constructor(opts?: {
|
|
20
21
|
readonly pendingWriteTtlMs?: number;
|
|
21
22
|
});
|
|
23
|
+
get lastSyncedAt(): Date | null;
|
|
24
|
+
setLastSyncedAt(at?: Date): void;
|
|
22
25
|
load(recipes: ReadonlyArray<Recipe>, categories: ReadonlyArray<Category>): void;
|
|
23
26
|
getAll(): Array<Recipe>;
|
|
24
27
|
get size(): number;
|
|
@@ -2,9 +2,16 @@ import { EntityStore } from "../entity/index.js";
|
|
|
2
2
|
import { parseDuration } from "../utils/duration.js";
|
|
3
3
|
export class RecipeStore extends EntityStore {
|
|
4
4
|
categories = new Map();
|
|
5
|
+
_lastSyncedAt = null;
|
|
5
6
|
constructor(opts) {
|
|
6
7
|
super(opts ?? {});
|
|
7
8
|
}
|
|
9
|
+
get lastSyncedAt() {
|
|
10
|
+
return this._lastSyncedAt;
|
|
11
|
+
}
|
|
12
|
+
setLastSyncedAt(at = new Date()) {
|
|
13
|
+
this._lastSyncedAt = at;
|
|
14
|
+
}
|
|
8
15
|
load(recipes, categories) {
|
|
9
16
|
this.baseLoad(recipes);
|
|
10
17
|
this.setCategories(categories);
|
package/dist/entity/index.d.ts
CHANGED
package/dist/entity/index.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EntityStore } from "./store.js";
|
|
2
|
+
export declare abstract class TombstoneEntityStore<T extends {
|
|
3
|
+
uid: UID;
|
|
4
|
+
}, UID extends string> extends EntityStore<T, UID> {
|
|
5
|
+
private readonly _tombstones;
|
|
6
|
+
load(items: ReadonlyArray<T>): void;
|
|
7
|
+
set(item: T): void;
|
|
8
|
+
delete(uid: UID): void;
|
|
9
|
+
isTombstone(uid: UID): boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EntityStore } from "./store.js";
|
|
2
|
+
export class TombstoneEntityStore extends EntityStore {
|
|
3
|
+
_tombstones = new Set();
|
|
4
|
+
load(items) {
|
|
5
|
+
this.baseLoad(items);
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
this._tombstones.delete(this.getUid(item));
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
set(item) {
|
|
11
|
+
super.set(item);
|
|
12
|
+
this._tombstones.delete(this.getUid(item));
|
|
13
|
+
}
|
|
14
|
+
delete(uid) {
|
|
15
|
+
this._tombstones.add(uid);
|
|
16
|
+
super.delete(uid);
|
|
17
|
+
}
|
|
18
|
+
isTombstone(uid) {
|
|
19
|
+
return this._tombstones.has(uid);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/paprika/client.d.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typed HTTP client for the Paprika Cloud Sync API.
|
|
3
|
-
*
|
|
4
|
-
* Encapsulates authentication against the v1 login endpoint
|
|
5
|
-
* and resilient request execution against the v2 data endpoint.
|
|
6
|
-
*
|
|
7
|
-
* Provides recipe and category read methods, plus write methods
|
|
8
|
-
* added in P1-U07 (saveRecipe, deleteRecipe, notifySync).
|
|
9
|
-
*/
|
|
10
1
|
import type { Logger } from "pino";
|
|
11
|
-
import type { Category, PantryItem, Recipe, RecipeEntry, RecipeUid } from "./types.js";
|
|
2
|
+
import type { Aisle, Category, GroceryIngredient, GroceryItem, GroceryList, PantryItem, Recipe, RecipeEntry, RecipeUid } from "./types.js";
|
|
12
3
|
export declare class PaprikaClient {
|
|
13
4
|
private readonly email;
|
|
14
5
|
private readonly password;
|
|
@@ -24,12 +15,20 @@ export declare class PaprikaClient {
|
|
|
24
15
|
getRecipe(uid: string): Promise<Recipe>;
|
|
25
16
|
getRecipes(uids: ReadonlyArray<string>): Promise<Array<Recipe>>;
|
|
26
17
|
listCategories(): Promise<Array<Category>>;
|
|
18
|
+
listAisles(): Promise<Array<Aisle>>;
|
|
19
|
+
listGroceryLists(): Promise<Array<GroceryList>>;
|
|
20
|
+
listGroceryItems(): Promise<Array<GroceryItem>>;
|
|
21
|
+
listGroceryIngredients(): Promise<Array<GroceryIngredient>>;
|
|
27
22
|
listPantry(): Promise<Array<PantryItem>>;
|
|
28
23
|
saveRecipe(recipe: Readonly<Recipe>): Promise<Recipe>;
|
|
29
|
-
|
|
24
|
+
saveAisle(aisle: Readonly<Aisle>): Promise<Aisle>;
|
|
25
|
+
savePantryItems(items: ReadonlyArray<Readonly<PantryItem>>): Promise<ReadonlyArray<PantryItem>>;
|
|
26
|
+
saveGroceryList(list: Readonly<GroceryList>): Promise<GroceryList>;
|
|
27
|
+
saveGroceryItems(items: ReadonlyArray<Readonly<GroceryItem>>): Promise<ReadonlyArray<GroceryItem>>;
|
|
28
|
+
saveGroceryIngredient(ingredient: Readonly<GroceryIngredient>): Promise<GroceryIngredient>;
|
|
30
29
|
notifySync(): Promise<void>;
|
|
31
30
|
deleteRecipe(uid: RecipeUid): Promise<void>;
|
|
32
|
-
private
|
|
33
|
-
private
|
|
31
|
+
private buildEntityFormData;
|
|
32
|
+
private postEntities;
|
|
34
33
|
private request;
|
|
35
34
|
}
|