@absolutejs/sync-pack-favorites 0.1.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 ADDED
@@ -0,0 +1,128 @@
1
+ # @absolutejs/sync-pack-favorites
2
+
3
+ Per-actor saved-resources pack for
4
+ [`@absolutejs/sync`](https://github.com/absolutejs/sync). Each actor sees
5
+ only their own rows; favoriting is idempotent (deterministic row id);
6
+ optional join collection pairs each favorite with the host's resource
7
+ row in one live subscription.
8
+
9
+ ```bash
10
+ bun add @absolutejs/sync-pack-favorites
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createSyncEngine } from '@absolutejs/sync/engine';
17
+ import { createFavoritesPack } from '@absolutejs/sync-pack-favorites';
18
+
19
+ const engine = createSyncEngine();
20
+ engine.registerPack(
21
+ createFavoritesPack({
22
+ getActorId: (ctx) => ctx.session.userId,
23
+ }),
24
+ );
25
+ ```
26
+
27
+ Then from the client:
28
+
29
+ ```ts
30
+ // Toggle is the easy default — one round-trip, returns the new state.
31
+ const { favorited } = await store.mutate({
32
+ args: { resourceKind: 'doc', resourceId: 'doc-123' },
33
+ name: 'favorites:toggle',
34
+ });
35
+
36
+ // Or favorite / unfavorite explicitly.
37
+ await store.mutate({ args: { resourceKind: 'doc', resourceId: 'doc-123' }, name: 'favorites:favorite' });
38
+ await store.mutate({ args: { resourceKind: 'doc', resourceId: 'doc-123' }, name: 'favorites:unfavorite' });
39
+ ```
40
+
41
+ ## The pack exposes
42
+
43
+ | Surface | Name | What it does |
44
+ | ----------- | ----------------------------- | ----------------------------------------------------------- |
45
+ | Collection | `favorites` | Per-actor list. Optional `params.resourceKind` filter |
46
+ | Mutation | `favorites:favorite` | Idempotent insert. Subsequent calls are no-ops |
47
+ | Mutation | `favorites:unfavorite` | Idempotent delete. No-op if the row never existed |
48
+ | Mutation | `favorites:toggle` | Insert if missing, delete if present. Returns `{ favorited }` |
49
+ | Collection | `favorites-with-resource` (opt) | Join with the host's resource table — see below |
50
+
51
+ ## Row shape
52
+
53
+ ```ts
54
+ type FavoriteRow = {
55
+ id: string; // `${actorId}:${resourceKind}:${resourceId}` — deterministic
56
+ actorId: string;
57
+ resourceKind: string; // app-level: "doc" | "task" | "issue" | ...
58
+ resourceId: string;
59
+ createdAt: number;
60
+ };
61
+ ```
62
+
63
+ The deterministic id means the same `(actor, kind, resource)` triple
64
+ always maps to the same row — so duplicate `favorite` calls are
65
+ idempotent at the storage layer, not just behaviorally.
66
+
67
+ ## Optional: `favorites-with-resource` join
68
+
69
+ When you set `joinResources`, the pack additionally registers a join
70
+ collection that pairs each favorite with the host's resource row (same
71
+ pattern as comments-with-author from sync-pack-comments).
72
+
73
+ ```ts
74
+ type DocRow = { id: string; title: string };
75
+
76
+ engine.registerReader('docs', { all: () => db.docs.list() });
77
+ engine.registerPack(
78
+ createFavoritesPack<MyCtx, DocRow>({
79
+ getActorId,
80
+ joinResources: {
81
+ table: 'docs',
82
+ // Default (row) => row.id; override if your resource id key isn't `id`.
83
+ // key: (doc) => doc.docId,
84
+ // Required: host supplies the resource-side hydrate.
85
+ hydrate: () => db.docs.list(),
86
+ },
87
+ }),
88
+ );
89
+
90
+ const view = await engine.subscribe<
91
+ FavoriteRow & { resource: DocRow },
92
+ { resourceKind?: string }
93
+ >({
94
+ collection: 'favorites-with-resource',
95
+ params: { resourceKind: 'doc' },
96
+ ctx,
97
+ onDiff: rerender,
98
+ });
99
+ // view.initial[0] === { ...favorite, resource: { id, title } }
100
+ ```
101
+
102
+ Inner join — favorites whose resource has been deleted drop out of the
103
+ join (but stay in the base `favorites` collection).
104
+
105
+ ## Storage
106
+
107
+ Default: per-instance in-memory store. For a persistent backend, pass a
108
+ custom `store`:
109
+
110
+ ```ts
111
+ import { type FavoritesStore } from '@absolutejs/sync-pack-favorites';
112
+
113
+ const store: FavoritesStore = {
114
+ getById: (id) => /* SELECT * FROM favorites WHERE id = $1 */,
115
+ reader: { all: () => /* SELECT */ },
116
+ writer: { insert, update, delete },
117
+ };
118
+ ```
119
+
120
+ ## Multiple instances
121
+
122
+ Pass a `prefix` to coexist with another favorites pack instance (e.g. a
123
+ "team" set vs a "private" set):
124
+
125
+ ```ts
126
+ engine.registerPack(createFavoritesPack({ prefix: 'team_', /* ... */ }));
127
+ engine.registerPack(createFavoritesPack({ prefix: 'private_', /* ... */ }));
128
+ ```
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `@absolutejs/sync-pack-favorites` — per-actor saved resources.
3
+ *
4
+ * Owns a `favorites` table; rows are `(actorId, resourceKind, resourceId)`
5
+ * triples with a stable primary key derived from them, so toggling
6
+ * favorite/unfavorite is an upsert/delete with no extra bookkeeping.
7
+ *
8
+ * Each actor sees only their own rows. Optional `joinResources` registers
9
+ * a `favorites-with-resource` join collection that pairs each favorite
10
+ * with the host's resource row (same shape as comments-with-author from
11
+ * the comments pack). Useful when the UI wants "title + body + favorited?"
12
+ * in one subscription.
13
+ */
14
+ import { type CollectionContext, type SyncPack, type TableReader, type TableWriter } from '@absolutejs/sync/engine';
15
+ /** A single favorited resource for a single actor. */
16
+ export type FavoriteRow = {
17
+ /** `${actorId}:${resourceKind}:${resourceId}` — deterministic so toggling is idempotent. */
18
+ id: string;
19
+ actorId: string;
20
+ /** App-level resource type ("doc", "task", "issue", ...). Lets one inbox span multiple kinds. */
21
+ resourceKind: string;
22
+ resourceId: string;
23
+ createdAt: number;
24
+ };
25
+ /** A `FavoriteRow` paired with the host's resource row, from the optional join. */
26
+ export type FavoriteWithResource<ResourceRow = unknown> = FavoriteRow & {
27
+ resource: ResourceRow;
28
+ };
29
+ export type FavoritesStore = {
30
+ reader: TableReader<CollectionContext>;
31
+ writer: TableWriter<FavoriteRow, CollectionContext, unknown>;
32
+ getById: (id: string) => FavoriteRow | undefined;
33
+ };
34
+ export declare const createInMemoryFavoritesStore: () => FavoritesStore;
35
+ export type JoinResourcesConfig<Ctx, ResourceRow = unknown> = {
36
+ /**
37
+ * The host's resource table name. Surfaced in `readsTables` so the
38
+ * dependency graph is reviewable. Required when the join is enabled
39
+ * (no default — many apps have multiple resource tables).
40
+ */
41
+ table: string;
42
+ /** Get the host resource's id (the join field). Default `(row) => row.id`. */
43
+ key?: (resource: ResourceRow) => string;
44
+ /**
45
+ * Host-side hydrate for the resources side of the join. The pack
46
+ * passes the subscription params + ctx; this returns candidate
47
+ * resources. The engine inner-joins on
48
+ * `favorite.resourceId === resource.id`.
49
+ */
50
+ hydrate: (params: {
51
+ actorId: string | undefined;
52
+ resourceKind?: string;
53
+ }, ctx: Ctx) => Iterable<ResourceRow> | Promise<Iterable<ResourceRow>>;
54
+ };
55
+ export type FavoritesPackConfig<Ctx = CollectionContext, ResourceRow = unknown> = {
56
+ prefix?: string;
57
+ getActorId?: (ctx: Ctx) => string | undefined;
58
+ /** Custom storage adapter. Default per-instance in-memory. */
59
+ store?: FavoritesStore;
60
+ /** Wall-clock. Default `Date.now`. */
61
+ now?: () => number;
62
+ /**
63
+ * Optional join collection. When set, registers
64
+ * `favorites-with-resource` and adds the resource table to
65
+ * `readsTables`.
66
+ */
67
+ joinResources?: JoinResourcesConfig<Ctx, ResourceRow>;
68
+ };
69
+ /**
70
+ * Build a {@link SyncPack} that exposes a per-actor list of favorited
71
+ * host-side resources. Optional join collection pairs each favorite with
72
+ * its host resource row.
73
+ */
74
+ export declare const createFavoritesPack: <Ctx = CollectionContext, ResourceRow = unknown>(config?: FavoritesPackConfig<Ctx, ResourceRow>) => SyncPack;
package/dist/index.js ADDED
@@ -0,0 +1,218 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import {
4
+ defineCollection,
5
+ defineJoinCollection,
6
+ defineMutation,
7
+ defineSchema,
8
+ defineSyncPack,
9
+ field,
10
+ UnauthorizedError
11
+ } from "@absolutejs/sync/engine";
12
+ var createInMemoryFavoritesStore = () => {
13
+ const rows = new Map;
14
+ return {
15
+ getById: (id) => rows.get(id),
16
+ reader: {
17
+ all: () => [...rows.values()]
18
+ },
19
+ writer: {
20
+ delete: (row) => {
21
+ rows.delete(row.id);
22
+ },
23
+ insert: (data) => {
24
+ rows.set(data.id, data);
25
+ return data;
26
+ },
27
+ update: (data) => {
28
+ const prior = rows.get(data.id);
29
+ const merged = { ...prior ?? {}, ...data };
30
+ rows.set(data.id, merged);
31
+ return merged;
32
+ }
33
+ }
34
+ };
35
+ };
36
+ var resolveActor = (getActorId, ctx) => {
37
+ const actorId = getActorId(ctx);
38
+ if (actorId === undefined || actorId === "") {
39
+ throw new UnauthorizedError("favorites mutation (no actor id)");
40
+ }
41
+ return actorId;
42
+ };
43
+ var rowId = (actorId, resourceKind, resourceId) => `${actorId}:${resourceKind}:${resourceId}`;
44
+ var createFavoritesPack = (config = {}) => {
45
+ const prefix = config.prefix ?? "";
46
+ const table = `${prefix}favorites`;
47
+ const collectionName = table;
48
+ const joinCollectionName = `${prefix}favorites-with-resource`;
49
+ const favoriteMutationName = `${prefix}favorites:favorite`;
50
+ const unfavoriteMutationName = `${prefix}favorites:unfavorite`;
51
+ const toggleMutationName = `${prefix}favorites:toggle`;
52
+ const store = config.store ?? createInMemoryFavoritesStore();
53
+ const now = config.now ?? Date.now;
54
+ const getActorId = config.getActorId ?? ((ctx) => ctx.userId);
55
+ const joinResources = config.joinResources;
56
+ const resourceKey = joinResources?.key ?? ((row) => row.id);
57
+ const pack = {
58
+ name: "@absolutejs/sync-pack-favorites",
59
+ ownsTables: [table],
60
+ readsTables: joinResources === undefined ? [] : [joinResources.table],
61
+ version: "0.1.0",
62
+ schemas: defineSchema({
63
+ [table]: {
64
+ fields: {
65
+ id: field.string,
66
+ actorId: field.string,
67
+ resourceKind: field.string,
68
+ resourceId: field.string,
69
+ createdAt: field.number
70
+ }
71
+ }
72
+ }),
73
+ readers: { [table]: store.reader },
74
+ writers: { [table]: store.writer },
75
+ permissions: {
76
+ [table]: {
77
+ read: (ctx, row) => {
78
+ const callerId = getActorId(ctx);
79
+ return callerId !== undefined && row.actorId === callerId;
80
+ },
81
+ insert: (ctx, row) => {
82
+ const callerId = getActorId(ctx);
83
+ return callerId !== undefined && row.actorId === callerId;
84
+ },
85
+ update: (ctx, row) => {
86
+ const callerId = getActorId(ctx);
87
+ return callerId !== undefined && row.actorId === callerId;
88
+ },
89
+ delete: (ctx, row) => {
90
+ const callerId = getActorId(ctx);
91
+ if (callerId === undefined)
92
+ return false;
93
+ const ownerId = row.actorId ?? (() => {
94
+ const id = row.id;
95
+ return id === undefined ? undefined : store.getById(id)?.actorId;
96
+ })();
97
+ return ownerId === callerId;
98
+ }
99
+ }
100
+ },
101
+ collections: [
102
+ defineCollection({
103
+ name: collectionName,
104
+ tables: [table],
105
+ key: (row) => row.id,
106
+ hydrate: (params, ctx) => {
107
+ const callerId = getActorId(ctx);
108
+ if (callerId === undefined)
109
+ return [];
110
+ return store.reader.all(ctx).filter((row) => row.actorId === callerId && (params.resourceKind === undefined || row.resourceKind === params.resourceKind));
111
+ },
112
+ match: (row, params, ctx) => {
113
+ const callerId = getActorId(ctx);
114
+ if (callerId === undefined || row.actorId !== callerId)
115
+ return false;
116
+ return params.resourceKind === undefined || row.resourceKind === params.resourceKind;
117
+ },
118
+ authorize: (_params, ctx) => getActorId(ctx) !== undefined
119
+ })
120
+ ],
121
+ mutations: [
122
+ defineMutation({
123
+ name: favoriteMutationName,
124
+ handler: async (args, ctx, actions) => {
125
+ const actorId = resolveActor(getActorId, ctx);
126
+ const id = rowId(actorId, args.resourceKind, args.resourceId);
127
+ const existing = store.getById(id);
128
+ if (existing !== undefined)
129
+ return existing;
130
+ const row = {
131
+ actorId,
132
+ createdAt: now(),
133
+ id,
134
+ resourceId: args.resourceId,
135
+ resourceKind: args.resourceKind
136
+ };
137
+ return await actions.insert(table, row);
138
+ }
139
+ }),
140
+ defineMutation({
141
+ name: unfavoriteMutationName,
142
+ handler: async (args, ctx, actions) => {
143
+ const actorId = resolveActor(getActorId, ctx);
144
+ const id = rowId(actorId, args.resourceKind, args.resourceId);
145
+ if (store.getById(id) === undefined)
146
+ return;
147
+ await actions.delete(table, { id });
148
+ }
149
+ }),
150
+ defineMutation({
151
+ name: toggleMutationName,
152
+ handler: async (args, ctx, actions) => {
153
+ const actorId = resolveActor(getActorId, ctx);
154
+ const id = rowId(actorId, args.resourceKind, args.resourceId);
155
+ const existing = store.getById(id);
156
+ if (existing === undefined) {
157
+ const row = {
158
+ actorId,
159
+ createdAt: now(),
160
+ id,
161
+ resourceId: args.resourceId,
162
+ resourceKind: args.resourceKind
163
+ };
164
+ await actions.insert(table, row);
165
+ return { favorited: true };
166
+ }
167
+ await actions.delete(table, { id });
168
+ return { favorited: false };
169
+ }
170
+ })
171
+ ]
172
+ };
173
+ if (joinResources !== undefined) {
174
+ const resourceHydrate = joinResources.hydrate;
175
+ pack.joinCollections = [
176
+ defineJoinCollection({
177
+ name: joinCollectionName,
178
+ key: (out) => out.id,
179
+ left: {
180
+ table,
181
+ hydrate: (params, ctx) => {
182
+ const callerId = getActorId(ctx);
183
+ if (callerId === undefined)
184
+ return [];
185
+ return store.reader.all(ctx).filter((row) => row.actorId === callerId && (params.resourceKind === undefined || row.resourceKind === params.resourceKind));
186
+ },
187
+ key: (row) => row.id,
188
+ on: (row) => row.resourceId,
189
+ match: (row, params, ctx) => {
190
+ const callerId = getActorId(ctx);
191
+ if (callerId === undefined || row.actorId !== callerId)
192
+ return false;
193
+ return params.resourceKind === undefined || row.resourceKind === params.resourceKind;
194
+ }
195
+ },
196
+ right: {
197
+ table: joinResources.table,
198
+ hydrate: (params, ctx) => resourceHydrate({
199
+ actorId: getActorId(ctx),
200
+ resourceKind: params.resourceKind
201
+ }, ctx),
202
+ key: (resource) => resourceKey(resource),
203
+ on: (resource) => resourceKey(resource)
204
+ },
205
+ select: (favorite, resource) => ({ ...favorite, resource }),
206
+ authorize: (_params, ctx) => getActorId(ctx) !== undefined
207
+ })
208
+ ];
209
+ }
210
+ return defineSyncPack(pack);
211
+ };
212
+ export {
213
+ createInMemoryFavoritesStore,
214
+ createFavoritesPack
215
+ };
216
+
217
+ //# debugId=E27906193B10A86564756E2164756E21
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `@absolutejs/sync-pack-favorites` — per-actor saved resources.\n *\n * Owns a `favorites` table; rows are `(actorId, resourceKind, resourceId)`\n * triples with a stable primary key derived from them, so toggling\n * favorite/unfavorite is an upsert/delete with no extra bookkeeping.\n *\n * Each actor sees only their own rows. Optional `joinResources` registers\n * a `favorites-with-resource` join collection that pairs each favorite\n * with the host's resource row (same shape as comments-with-author from\n * the comments pack). Useful when the UI wants \"title + body + favorited?\"\n * in one subscription.\n */\n\nimport {\n\tdefineCollection,\n\tdefineJoinCollection,\n\tdefineMutation,\n\tdefineSchema,\n\tdefineSyncPack,\n\tfield,\n\tUnauthorizedError,\n\ttype CollectionContext,\n\ttype SyncPack,\n\ttype TableReader,\n\ttype TableWriter\n} from '@absolutejs/sync/engine';\n\n/** A single favorited resource for a single actor. */\nexport type FavoriteRow = {\n\t/** `${actorId}:${resourceKind}:${resourceId}` — deterministic so toggling is idempotent. */\n\tid: string;\n\tactorId: string;\n\t/** App-level resource type (\"doc\", \"task\", \"issue\", ...). Lets one inbox span multiple kinds. */\n\tresourceKind: string;\n\tresourceId: string;\n\tcreatedAt: number;\n};\n\n/** A `FavoriteRow` paired with the host's resource row, from the optional join. */\nexport type FavoriteWithResource<ResourceRow = unknown> = FavoriteRow & {\n\tresource: ResourceRow;\n};\n\nexport type FavoritesStore = {\n\treader: TableReader<CollectionContext>;\n\twriter: TableWriter<FavoriteRow, CollectionContext, unknown>;\n\tgetById: (id: string) => FavoriteRow | undefined;\n};\n\nexport const createInMemoryFavoritesStore = (): FavoritesStore => {\n\tconst rows = new Map<string, FavoriteRow>();\n\treturn {\n\t\tgetById: (id) => rows.get(id),\n\t\treader: {\n\t\t\tall: () => [...rows.values()]\n\t\t},\n\t\twriter: {\n\t\t\tdelete: (row) => {\n\t\t\t\trows.delete((row as { id: string }).id);\n\t\t\t},\n\t\t\tinsert: (data) => {\n\t\t\t\trows.set(data.id, data);\n\t\t\t\treturn data;\n\t\t\t},\n\t\t\tupdate: (data) => {\n\t\t\t\tconst prior = rows.get(data.id);\n\t\t\t\tconst merged = { ...(prior ?? {}), ...data } as FavoriteRow;\n\t\t\t\trows.set(data.id, merged);\n\t\t\t\treturn merged;\n\t\t\t}\n\t\t}\n\t};\n};\n\nexport type JoinResourcesConfig<Ctx, ResourceRow = unknown> = {\n\t/**\n\t * The host's resource table name. Surfaced in `readsTables` so the\n\t * dependency graph is reviewable. Required when the join is enabled\n\t * (no default — many apps have multiple resource tables).\n\t */\n\ttable: string;\n\t/** Get the host resource's id (the join field). Default `(row) => row.id`. */\n\tkey?: (resource: ResourceRow) => string;\n\t/**\n\t * Host-side hydrate for the resources side of the join. The pack\n\t * passes the subscription params + ctx; this returns candidate\n\t * resources. The engine inner-joins on\n\t * `favorite.resourceId === resource.id`.\n\t */\n\thydrate: (\n\t\tparams: { actorId: string | undefined; resourceKind?: string },\n\t\tctx: Ctx\n\t) => Iterable<ResourceRow> | Promise<Iterable<ResourceRow>>;\n};\n\nexport type FavoritesPackConfig<Ctx = CollectionContext, ResourceRow = unknown> = {\n\tprefix?: string;\n\tgetActorId?: (ctx: Ctx) => string | undefined;\n\t/** Custom storage adapter. Default per-instance in-memory. */\n\tstore?: FavoritesStore;\n\t/** Wall-clock. Default `Date.now`. */\n\tnow?: () => number;\n\t/**\n\t * Optional join collection. When set, registers\n\t * `favorites-with-resource` and adds the resource table to\n\t * `readsTables`.\n\t */\n\tjoinResources?: JoinResourcesConfig<Ctx, ResourceRow>;\n};\n\nconst resolveActor = (\n\tgetActorId: NonNullable<FavoritesPackConfig['getActorId']>,\n\tctx: unknown\n): string => {\n\tconst actorId = getActorId(ctx as CollectionContext);\n\tif (actorId === undefined || actorId === '') {\n\t\tthrow new UnauthorizedError('favorites mutation (no actor id)');\n\t}\n\treturn actorId;\n};\n\nconst rowId = (\n\tactorId: string,\n\tresourceKind: string,\n\tresourceId: string\n): string => `${actorId}:${resourceKind}:${resourceId}`;\n\n/**\n * Build a {@link SyncPack} that exposes a per-actor list of favorited\n * host-side resources. Optional join collection pairs each favorite with\n * its host resource row.\n */\nexport const createFavoritesPack = <\n\tCtx = CollectionContext,\n\tResourceRow = unknown\n>(\n\tconfig: FavoritesPackConfig<Ctx, ResourceRow> = {}\n): SyncPack => {\n\tconst prefix = config.prefix ?? '';\n\tconst table = `${prefix}favorites`;\n\tconst collectionName = table;\n\tconst joinCollectionName = `${prefix}favorites-with-resource`;\n\tconst favoriteMutationName = `${prefix}favorites:favorite`;\n\tconst unfavoriteMutationName = `${prefix}favorites:unfavorite`;\n\tconst toggleMutationName = `${prefix}favorites:toggle`;\n\tconst store = config.store ?? createInMemoryFavoritesStore();\n\tconst now = config.now ?? Date.now;\n\tconst getActorId = (config.getActorId ??\n\t\t((ctx: CollectionContext) =>\n\t\t\t(ctx as { userId?: string }).userId)) as (\n\t\tctx: CollectionContext\n\t) => string | undefined;\n\tconst joinResources = config.joinResources;\n\tconst resourceKey = (joinResources?.key ?? ((row: unknown) =>\n\t\t(row as { id: string }).id)) as (resource: unknown) => string;\n\n\ttype Params = { resourceKind?: string };\n\n\tconst pack: SyncPack = {\n\t\tname: '@absolutejs/sync-pack-favorites',\n\t\townsTables: [table],\n\t\treadsTables:\n\t\t\tjoinResources === undefined ? [] : [joinResources.table],\n\t\tversion: '0.1.0',\n\n\t\tschemas: defineSchema({\n\t\t\t[table]: {\n\t\t\t\tfields: {\n\t\t\t\t\tid: field.string,\n\t\t\t\t\tactorId: field.string,\n\t\t\t\t\tresourceKind: field.string,\n\t\t\t\t\tresourceId: field.string,\n\t\t\t\t\tcreatedAt: field.number\n\t\t\t\t}\n\t\t\t}\n\t\t}),\n\n\t\treaders: { [table]: store.reader },\n\t\twriters: { [table]: store.writer },\n\n\t\tpermissions: {\n\t\t\t[table]: {\n\t\t\t\tread: (ctx: unknown, row: FavoriteRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\treturn callerId !== undefined && row.actorId === callerId;\n\t\t\t\t},\n\t\t\t\tinsert: (ctx: unknown, row: FavoriteRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\treturn callerId !== undefined && row.actorId === callerId;\n\t\t\t\t},\n\t\t\t\tupdate: (ctx: unknown, row: FavoriteRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\treturn callerId !== undefined && row.actorId === callerId;\n\t\t\t\t},\n\t\t\t\tdelete: (ctx: unknown, row: FavoriteRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\tif (callerId === undefined) return false;\n\t\t\t\t\tconst ownerId =\n\t\t\t\t\t\t(row as { actorId?: string }).actorId ??\n\t\t\t\t\t\t(() => {\n\t\t\t\t\t\t\tconst id = (row as { id?: string }).id;\n\t\t\t\t\t\t\treturn id === undefined\n\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t: store.getById(id)?.actorId;\n\t\t\t\t\t\t})();\n\t\t\t\t\treturn ownerId === callerId;\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tcollections: [\n\t\t\tdefineCollection<FavoriteRow, Params, CollectionContext>({\n\t\t\t\tname: collectionName,\n\t\t\t\ttables: [table],\n\t\t\t\tkey: (row) => row.id,\n\t\t\t\thydrate: (params, ctx) => {\n\t\t\t\t\tconst callerId = getActorId(ctx);\n\t\t\t\t\tif (callerId === undefined) return [];\n\t\t\t\t\treturn (store.reader.all(ctx) as FavoriteRow[]).filter(\n\t\t\t\t\t\t(row) =>\n\t\t\t\t\t\t\trow.actorId === callerId &&\n\t\t\t\t\t\t\t(params.resourceKind === undefined ||\n\t\t\t\t\t\t\t\trow.resourceKind === params.resourceKind)\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tmatch: (row, params, ctx) => {\n\t\t\t\t\tconst callerId = getActorId(ctx);\n\t\t\t\t\tif (callerId === undefined || row.actorId !== callerId)\n\t\t\t\t\t\treturn false;\n\t\t\t\t\treturn (\n\t\t\t\t\t\tparams.resourceKind === undefined ||\n\t\t\t\t\t\trow.resourceKind === params.resourceKind\n\t\t\t\t\t);\n\t\t\t\t},\n\t\t\t\tauthorize: (_params, ctx) =>\n\t\t\t\t\tgetActorId(ctx as CollectionContext) !== undefined\n\t\t\t})\n\t\t],\n\n\t\tmutations: [\n\t\t\tdefineMutation<\n\t\t\t\t{ resourceKind: string; resourceId: string },\n\t\t\t\tCollectionContext,\n\t\t\t\tFavoriteRow\n\t\t\t>({\n\t\t\t\tname: favoriteMutationName,\n\t\t\t\thandler: async (args, ctx, actions) => {\n\t\t\t\t\tconst actorId = resolveActor(getActorId, ctx);\n\t\t\t\t\tconst id = rowId(actorId, args.resourceKind, args.resourceId);\n\t\t\t\t\tconst existing = store.getById(id);\n\t\t\t\t\tif (existing !== undefined) return existing;\n\t\t\t\t\tconst row: FavoriteRow = {\n\t\t\t\t\t\tactorId,\n\t\t\t\t\t\tcreatedAt: now(),\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tresourceId: args.resourceId,\n\t\t\t\t\t\tresourceKind: args.resourceKind\n\t\t\t\t\t};\n\t\t\t\t\treturn (await actions.insert(table, row)) as FavoriteRow;\n\t\t\t\t}\n\t\t\t}),\n\t\t\tdefineMutation<\n\t\t\t\t{ resourceKind: string; resourceId: string },\n\t\t\t\tCollectionContext,\n\t\t\t\tvoid\n\t\t\t>({\n\t\t\t\tname: unfavoriteMutationName,\n\t\t\t\thandler: async (args, ctx, actions) => {\n\t\t\t\t\tconst actorId = resolveActor(getActorId, ctx);\n\t\t\t\t\tconst id = rowId(actorId, args.resourceKind, args.resourceId);\n\t\t\t\t\tif (store.getById(id) === undefined) return;\n\t\t\t\t\tawait actions.delete(table, { id });\n\t\t\t\t}\n\t\t\t}),\n\t\t\t// Convenience: toggle is one round-trip from the client.\n\t\t\tdefineMutation<\n\t\t\t\t{ resourceKind: string; resourceId: string },\n\t\t\t\tCollectionContext,\n\t\t\t\t{ favorited: boolean }\n\t\t\t>({\n\t\t\t\tname: toggleMutationName,\n\t\t\t\thandler: async (args, ctx, actions) => {\n\t\t\t\t\tconst actorId = resolveActor(getActorId, ctx);\n\t\t\t\t\tconst id = rowId(actorId, args.resourceKind, args.resourceId);\n\t\t\t\t\tconst existing = store.getById(id);\n\t\t\t\t\tif (existing === undefined) {\n\t\t\t\t\t\tconst row: FavoriteRow = {\n\t\t\t\t\t\t\tactorId,\n\t\t\t\t\t\t\tcreatedAt: now(),\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\tresourceId: args.resourceId,\n\t\t\t\t\t\t\tresourceKind: args.resourceKind\n\t\t\t\t\t\t};\n\t\t\t\t\t\tawait actions.insert(table, row);\n\t\t\t\t\t\treturn { favorited: true };\n\t\t\t\t\t}\n\t\t\t\t\tawait actions.delete(table, { id });\n\t\t\t\t\treturn { favorited: false };\n\t\t\t\t}\n\t\t\t})\n\t\t]\n\t};\n\n\tif (joinResources !== undefined) {\n\t\tconst resourceHydrate = joinResources.hydrate;\n\t\tpack.joinCollections = [\n\t\t\tdefineJoinCollection<\n\t\t\t\tFavoriteRow,\n\t\t\t\tunknown,\n\t\t\t\tFavoriteWithResource,\n\t\t\t\tParams,\n\t\t\t\tCollectionContext\n\t\t\t>({\n\t\t\t\tname: joinCollectionName,\n\t\t\t\tkey: (out) => out.id,\n\t\t\t\tleft: {\n\t\t\t\t\ttable,\n\t\t\t\t\thydrate: (params, ctx) => {\n\t\t\t\t\t\tconst callerId = getActorId(ctx);\n\t\t\t\t\t\tif (callerId === undefined) return [];\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\tstore.reader.all(ctx) as FavoriteRow[]\n\t\t\t\t\t\t).filter(\n\t\t\t\t\t\t\t(row) =>\n\t\t\t\t\t\t\t\trow.actorId === callerId &&\n\t\t\t\t\t\t\t\t(params.resourceKind === undefined ||\n\t\t\t\t\t\t\t\t\trow.resourceKind === params.resourceKind)\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t\tkey: (row) => row.id,\n\t\t\t\t\ton: (row) => row.resourceId,\n\t\t\t\t\tmatch: (row, params, ctx) => {\n\t\t\t\t\t\tconst callerId = getActorId(ctx);\n\t\t\t\t\t\tif (callerId === undefined || row.actorId !== callerId)\n\t\t\t\t\t\t\treturn false;\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\tparams.resourceKind === undefined ||\n\t\t\t\t\t\t\trow.resourceKind === params.resourceKind\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tright: {\n\t\t\t\t\ttable: joinResources.table,\n\t\t\t\t\thydrate: (params, ctx) =>\n\t\t\t\t\t\tresourceHydrate(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tactorId: getActorId(ctx),\n\t\t\t\t\t\t\t\tresourceKind: params.resourceKind\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tctx as Ctx\n\t\t\t\t\t\t) as Iterable<unknown>,\n\t\t\t\t\tkey: (resource) => resourceKey(resource),\n\t\t\t\t\ton: (resource) => resourceKey(resource)\n\t\t\t\t},\n\t\t\t\tselect: (favorite, resource) => ({ ...favorite, resource }),\n\t\t\t\tauthorize: (_params, ctx) =>\n\t\t\t\t\tgetActorId(ctx as CollectionContext) !== undefined\n\t\t\t})\n\t\t];\n\t}\n\n\treturn defineSyncPack(pack);\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "E27906193B10A86564756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@absolutejs/sync-pack-favorites",
3
+ "version": "0.1.0",
4
+ "description": "Per-actor saved-resources pack for @absolutejs/sync — favorite/unfavorite/list with optional join to host resources",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/absolutejs/sync-packs.git",
8
+ "directory": "favorites"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "type": "module",
14
+ "license": "CC BY-NC 4.0",
15
+ "author": "Alex Kahn",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "absolutejs",
21
+ "sync",
22
+ "sync-pack",
23
+ "favorites",
24
+ "bookmarks"
25
+ ],
26
+ "scripts": {
27
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun --external @absolutejs/sync && tsc --project tsconfig.build.json",
28
+ "test": "bun test",
29
+ "typecheck": "tsc --noEmit",
30
+ "format": "prettier --write \"./**/*.{ts,json,md}\"",
31
+ "release": "bun run format && bun run typecheck && bun run test && bun run build && bun publish"
32
+ },
33
+ "peerDependencies": {
34
+ "@absolutejs/sync": ">= 1.9.2"
35
+ },
36
+ "devDependencies": {
37
+ "@absolutejs/sync": "^1.9.2",
38
+ "@types/bun": "1.3.14",
39
+ "prettier": "3.5.3",
40
+ "typescript": "5.8.3"
41
+ },
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "import": "./dist/index.js",
46
+ "default": "./dist/index.js"
47
+ }
48
+ },
49
+ "files": ["dist", "README.md"]
50
+ }