@absolutejs/sync-pack-comments 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 +136 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +10 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# @absolutejs/sync-pack-comments
|
|
2
|
+
|
|
3
|
+
Threaded comments as a sync pack for
|
|
4
|
+
[`@absolutejs/sync`](https://github.com/absolutejs/sync). Per-resource ACL
|
|
5
|
+
injection, author/moderator gates, optional CRDT bodies. Plugs into a
|
|
6
|
+
`SyncEngine` with one `engine.registerPack(...)` call.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bun add @absolutejs/sync-pack-comments
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createSyncEngine } from '@absolutejs/sync/engine';
|
|
16
|
+
import { createCommentsPack } from '@absolutejs/sync-pack-comments';
|
|
17
|
+
|
|
18
|
+
const engine = createSyncEngine();
|
|
19
|
+
engine.registerPack(
|
|
20
|
+
createCommentsPack({
|
|
21
|
+
// REQUIRED: gate read access on a resource. The host knows which
|
|
22
|
+
// resources a given ctx can see; the pack does not duplicate that.
|
|
23
|
+
canReadResource: (resourceId, ctx) =>
|
|
24
|
+
hostAcl.canRead(resourceId, ctx.session.userId),
|
|
25
|
+
|
|
26
|
+
// REQUIRED in practice: how the pack reads the current actor id
|
|
27
|
+
// from your app's ctx. Default is `(ctx) => ctx.userId`.
|
|
28
|
+
getActorId: (ctx) => ctx.session.userId,
|
|
29
|
+
|
|
30
|
+
// OPTIONAL: moderator predicate. Used by comments:delete (author OR
|
|
31
|
+
// moderator can delete). Default `() => false`.
|
|
32
|
+
canModerate: (ctx) => ctx.session.isModerator,
|
|
33
|
+
|
|
34
|
+
// OPTIONAL: max thread depth (top-level = 0). Default 8.
|
|
35
|
+
maxDepth: 8,
|
|
36
|
+
|
|
37
|
+
// OPTIONAL: wire the comment body as a CRDT field via registerCrdt
|
|
38
|
+
// so concurrent edits merge instead of clobbering. Pass anything
|
|
39
|
+
// implementing `CrdtMergeable<T>` — e.g. yjsText from
|
|
40
|
+
// @absolutejs/sync-yjs. The pack does NOT import Yjs.
|
|
41
|
+
// bodyCrdt: yjsText,
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The pack exposes
|
|
47
|
+
|
|
48
|
+
| Surface | Name | What it does |
|
|
49
|
+
| ----------- | ------------------- | ----------------------------------------------------------------------- |
|
|
50
|
+
| Collection | `comments` | Subscribe with `params: { resourceId }` — returns the comment tree |
|
|
51
|
+
| Mutation | `comments:create` | Args: `{ resourceId, body, parentCommentId? }` — stamps `authorId` |
|
|
52
|
+
| Mutation | `comments:edit` | Args: `{ commentId, body }` — author only, stamps `editedAt` |
|
|
53
|
+
| Mutation | `comments:delete` | Args: `{ commentId }` — author or moderator |
|
|
54
|
+
|
|
55
|
+
When `bodyCrdt` is set, the engine auto-registers a `comments:merge`
|
|
56
|
+
mutation through `registerCrdt` — clients call that to merge CRDT body
|
|
57
|
+
updates concurrently with regular edits.
|
|
58
|
+
|
|
59
|
+
## Row shape
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
type CommentRow = {
|
|
63
|
+
id: string;
|
|
64
|
+
resourceId: string;
|
|
65
|
+
parentCommentId: string | null; // null on top-level; parent id on replies
|
|
66
|
+
authorId: string;
|
|
67
|
+
body: string;
|
|
68
|
+
depth: number; // 0 for top-level, parent.depth + 1 for replies
|
|
69
|
+
createdAt: number;
|
|
70
|
+
editedAt: number | null;
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The collection returns a flat list of rows for the resource; the client
|
|
75
|
+
builds the tree by walking `parentCommentId`. Depth is stored on the row
|
|
76
|
+
so a client can short-circuit-render without traversing the full chain.
|
|
77
|
+
|
|
78
|
+
## Storage
|
|
79
|
+
|
|
80
|
+
Default: per-instance in-memory store. To use a persistent backend (Drizzle,
|
|
81
|
+
Postgres, …), pass a custom `store`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import {
|
|
85
|
+
createCommentsPack,
|
|
86
|
+
type CommentsStore,
|
|
87
|
+
} from '@absolutejs/sync-pack-comments';
|
|
88
|
+
|
|
89
|
+
const store: CommentsStore = {
|
|
90
|
+
getById: (id) => /* SELECT * FROM comments WHERE id = $1 */,
|
|
91
|
+
reader: { all: () => /* SELECT * FROM comments */ },
|
|
92
|
+
writer: {
|
|
93
|
+
insert: (row) => /* INSERT */,
|
|
94
|
+
update: (row) => /* UPDATE */,
|
|
95
|
+
delete: (row) => /* DELETE WHERE id = $1 */,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
engine.registerPack(createCommentsPack({ store, canReadResource, getActorId }));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `getById` method is required (used by edit + delete to verify ownership
|
|
103
|
+
and by create to walk the parent chain for depth math).
|
|
104
|
+
|
|
105
|
+
## Multiple instances
|
|
106
|
+
|
|
107
|
+
To run two comments packs on the same engine (e.g. one per product
|
|
108
|
+
surface), pass a `prefix` to each. It scopes the owned table, the
|
|
109
|
+
collection, and the mutation names:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
engine.registerPack(createCommentsPack({ prefix: 'docs_', canReadResource, getActorId }));
|
|
113
|
+
engine.registerPack(createCommentsPack({ prefix: 'chat_', canReadResource, getActorId }));
|
|
114
|
+
|
|
115
|
+
// Tables: docs_comments, chat_comments
|
|
116
|
+
// Collections: docs_comments, chat_comments
|
|
117
|
+
// Mutations: docs_comments:create, chat_comments:create (etc.)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Composition
|
|
121
|
+
|
|
122
|
+
This pack composes with the rest of your sync graph via **subscriptions**.
|
|
123
|
+
A presence pack that wants to show "Alice is replying to this thread" should
|
|
124
|
+
subscribe to `comments` and `presence` separately — it should NOT call
|
|
125
|
+
`comments:*` from inside its own handler. See the design doc rules in
|
|
126
|
+
[`syncPacks.design.md`](https://github.com/absolutejs/sync/blob/main/src/engine/syncPacks.design.md).
|
|
127
|
+
|
|
128
|
+
## Planned for 0.2
|
|
129
|
+
|
|
130
|
+
- **`comments-with-author` join collection** — a `defineJoinCollection`
|
|
131
|
+
variant that includes display info from the host's user table. The
|
|
132
|
+
pack would `readsTables: [userTable]` and require the host to register
|
|
133
|
+
a reader for it. Deferred until a real consumer wants it, so we don't
|
|
134
|
+
over-engineer the right-side hydrate.
|
|
135
|
+
- **In-thread full-text search** via `registerSearch` on the comments
|
|
136
|
+
table.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@absolutejs/sync-pack-comments` — threaded comments on host-side resources
|
|
3
|
+
* as a sync pack.
|
|
4
|
+
*
|
|
5
|
+
* Owns a single `comments` table; reads no host tables of its own (the join
|
|
6
|
+
* to the host's user table is the pack's planned 0.2 feature — for now the
|
|
7
|
+
* `comments` collection emits `authorId` and the client looks up display
|
|
8
|
+
* info). Per-resource read access is gated through host-injected
|
|
9
|
+
* `canReadResource(resourceId, ctx)`. Edits are author-only; deletes are
|
|
10
|
+
* author or moderator. Optional `bodyCrdt` config wires the body field
|
|
11
|
+
* through `registerCrdt` for conflict-free editing.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createSyncEngine } from '@absolutejs/sync/engine';
|
|
16
|
+
* import { createCommentsPack } from '@absolutejs/sync-pack-comments';
|
|
17
|
+
*
|
|
18
|
+
* const engine = createSyncEngine();
|
|
19
|
+
* engine.registerPack(createCommentsPack({
|
|
20
|
+
* getActorId: (ctx) => ctx.session.userId,
|
|
21
|
+
* canReadResource: (resourceId, ctx) =>
|
|
22
|
+
* hostAcl.canRead(resourceId, ctx.session.userId),
|
|
23
|
+
* maxDepth: 8,
|
|
24
|
+
* }));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { type CollectionContext, type CrdtMergeable, type SyncPack, type TableReader, type TableWriter } from '@absolutejs/sync/engine';
|
|
28
|
+
/** A comment row stored in the pack's owned table. */
|
|
29
|
+
export type CommentRow = {
|
|
30
|
+
id: string;
|
|
31
|
+
resourceId: string;
|
|
32
|
+
/** Null on top-level comments, the parent's id on replies. */
|
|
33
|
+
parentCommentId: string | null;
|
|
34
|
+
authorId: string;
|
|
35
|
+
body: string;
|
|
36
|
+
/** Depth in the thread (0 for top-level, parent.depth + 1 for replies). */
|
|
37
|
+
depth: number;
|
|
38
|
+
createdAt: number;
|
|
39
|
+
/** Null until the comment is edited; otherwise the timestamp of the most
|
|
40
|
+
* recent edit. */
|
|
41
|
+
editedAt: number | null;
|
|
42
|
+
};
|
|
43
|
+
/** Storage adapter the pack uses for the comments table. */
|
|
44
|
+
export type CommentsStore = {
|
|
45
|
+
reader: TableReader<CollectionContext>;
|
|
46
|
+
writer: TableWriter<CommentRow, CollectionContext, unknown>;
|
|
47
|
+
/** Lookup by row id; used by edit + delete mutations to verify ownership
|
|
48
|
+
* and by create to walk the parent chain for depth math. */
|
|
49
|
+
getById: (id: string) => CommentRow | undefined;
|
|
50
|
+
};
|
|
51
|
+
/** Default in-memory store. Each pack instance creates its own. */
|
|
52
|
+
export declare const createInMemoryCommentsStore: () => CommentsStore;
|
|
53
|
+
/** Thrown when a reply would exceed the configured `maxDepth`. */
|
|
54
|
+
export declare class CommentDepthExceededError extends Error {
|
|
55
|
+
readonly maxDepth: number;
|
|
56
|
+
readonly attemptedDepth: number;
|
|
57
|
+
constructor(maxDepth: number, attemptedDepth: number);
|
|
58
|
+
}
|
|
59
|
+
/** Thrown when the requested parent comment doesn't exist or doesn't match. */
|
|
60
|
+
export declare class CommentParentMismatchError extends Error {
|
|
61
|
+
readonly parentCommentId: string;
|
|
62
|
+
constructor(parentCommentId: string);
|
|
63
|
+
}
|
|
64
|
+
/** Factory config. `canReadResource` is the only required field. */
|
|
65
|
+
export type CommentsPackConfig<Ctx = CollectionContext> = {
|
|
66
|
+
/**
|
|
67
|
+
* Prefix applied to the owned table name AND every collection/mutation
|
|
68
|
+
* the pack exposes. Default `""`. Use a non-empty prefix when
|
|
69
|
+
* registering multiple comments packs on one engine (e.g. one per
|
|
70
|
+
* product surface).
|
|
71
|
+
*/
|
|
72
|
+
prefix?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Extract the current actor id from the app's ctx. Default:
|
|
75
|
+
* `(ctx) => ctx.userId`. **This is the only contract the pack assumes
|
|
76
|
+
* about the app's ctx**, alongside `canReadResource` / `canModerate`.
|
|
77
|
+
*/
|
|
78
|
+
getActorId?: (ctx: Ctx) => string | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Gate read access on a resource. The host knows which resources a
|
|
81
|
+
* given ctx can read — the pack does NOT duplicate the host's ACL.
|
|
82
|
+
* Required: pack reads return zero rows otherwise.
|
|
83
|
+
*/
|
|
84
|
+
canReadResource: (resourceId: string, ctx: Ctx) => boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Optional moderation predicate. Used by `comments:delete` — author OR
|
|
87
|
+
* moderator can delete. Default `() => false` (only authors delete).
|
|
88
|
+
*/
|
|
89
|
+
canModerate?: (ctx: Ctx) => boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Maximum thread depth (top-level = 0, first reply = 1, ...). Replies
|
|
92
|
+
* past this depth reject with {@link CommentDepthExceededError}.
|
|
93
|
+
* Default `8`.
|
|
94
|
+
*/
|
|
95
|
+
maxDepth?: number;
|
|
96
|
+
/**
|
|
97
|
+
* When provided, register the comment row's `body` field as a CRDT via
|
|
98
|
+
* `engine.registerCrdt`. The engine then merges concurrent edits
|
|
99
|
+
* instead of overwriting. Typically `yjsText` from
|
|
100
|
+
* `@absolutejs/sync-yjs` — the pack does NOT import Yjs; the host
|
|
101
|
+
* provides the mergeable.
|
|
102
|
+
*/
|
|
103
|
+
bodyCrdt?: CrdtMergeable<unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* Override the storage adapter. Default: a per-instance in-memory
|
|
106
|
+
* store. Pass your own for a persistent backend (Drizzle, Postgres,
|
|
107
|
+
* Redis, ...).
|
|
108
|
+
*/
|
|
109
|
+
store?: CommentsStore;
|
|
110
|
+
/** Wall-clock function. Default `Date.now`. */
|
|
111
|
+
now?: () => number;
|
|
112
|
+
/**
|
|
113
|
+
* Generate a fresh row id. Default `crypto.randomUUID()`. Override for
|
|
114
|
+
* deterministic ids in tests, or to route through your existing id
|
|
115
|
+
* scheme.
|
|
116
|
+
*/
|
|
117
|
+
newId?: () => string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Build a {@link SyncPack} that exposes threaded comments. Each call returns
|
|
121
|
+
* a fresh pack with its own store (unless a custom `store` is supplied).
|
|
122
|
+
*/
|
|
123
|
+
export declare const createCommentsPack: <Ctx = CollectionContext>(config: CommentsPackConfig<Ctx>) => SyncPack;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import {
|
|
4
|
+
defineCollection,
|
|
5
|
+
defineMutation,
|
|
6
|
+
defineSchema,
|
|
7
|
+
defineSyncPack,
|
|
8
|
+
field,
|
|
9
|
+
UnauthorizedError
|
|
10
|
+
} from "@absolutejs/sync/engine";
|
|
11
|
+
var createInMemoryCommentsStore = () => {
|
|
12
|
+
const rows = new Map;
|
|
13
|
+
return {
|
|
14
|
+
getById: (id) => rows.get(id),
|
|
15
|
+
reader: {
|
|
16
|
+
all: () => [...rows.values()]
|
|
17
|
+
},
|
|
18
|
+
writer: {
|
|
19
|
+
delete: (row) => {
|
|
20
|
+
rows.delete(row.id);
|
|
21
|
+
},
|
|
22
|
+
insert: (data) => {
|
|
23
|
+
rows.set(data.id, data);
|
|
24
|
+
return data;
|
|
25
|
+
},
|
|
26
|
+
update: (data) => {
|
|
27
|
+
const prior = rows.get(data.id);
|
|
28
|
+
const merged = { ...prior ?? {}, ...data };
|
|
29
|
+
rows.set(data.id, merged);
|
|
30
|
+
return merged;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
class CommentDepthExceededError extends Error {
|
|
37
|
+
maxDepth;
|
|
38
|
+
attemptedDepth;
|
|
39
|
+
constructor(maxDepth, attemptedDepth) {
|
|
40
|
+
super(`Comment reply rejected: depth ${attemptedDepth} exceeds maxDepth ${maxDepth}`);
|
|
41
|
+
this.name = "CommentDepthExceededError";
|
|
42
|
+
this.maxDepth = maxDepth;
|
|
43
|
+
this.attemptedDepth = attemptedDepth;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class CommentParentMismatchError extends Error {
|
|
48
|
+
parentCommentId;
|
|
49
|
+
constructor(parentCommentId) {
|
|
50
|
+
super(`Parent comment ${parentCommentId} not found, or its resourceId doesn't match the reply's resourceId`);
|
|
51
|
+
this.name = "CommentParentMismatchError";
|
|
52
|
+
this.parentCommentId = parentCommentId;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
var DEFAULT_MAX_DEPTH = 8;
|
|
56
|
+
var resolveActor = (getActorId, ctx) => {
|
|
57
|
+
const actorId = getActorId(ctx);
|
|
58
|
+
if (actorId === undefined || actorId === "") {
|
|
59
|
+
throw new UnauthorizedError("comments mutation (no actor id)");
|
|
60
|
+
}
|
|
61
|
+
return actorId;
|
|
62
|
+
};
|
|
63
|
+
var createCommentsPack = (config) => {
|
|
64
|
+
const prefix = config.prefix ?? "";
|
|
65
|
+
const table = `${prefix}comments`;
|
|
66
|
+
const collectionName = table;
|
|
67
|
+
const createMutationName = `${prefix}comments:create`;
|
|
68
|
+
const editMutationName = `${prefix}comments:edit`;
|
|
69
|
+
const deleteMutationName = `${prefix}comments:delete`;
|
|
70
|
+
const maxDepth = config.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
71
|
+
const store = config.store ?? createInMemoryCommentsStore();
|
|
72
|
+
const now = config.now ?? Date.now;
|
|
73
|
+
const newId = config.newId ?? (() => globalThis.crypto.randomUUID());
|
|
74
|
+
const getActorId = config.getActorId ?? ((ctx) => ctx.userId);
|
|
75
|
+
const canReadResource = config.canReadResource;
|
|
76
|
+
const canModerate = config.canModerate ?? (() => false);
|
|
77
|
+
const pack = {
|
|
78
|
+
name: "@absolutejs/sync-pack-comments",
|
|
79
|
+
ownsTables: [table],
|
|
80
|
+
readsTables: [],
|
|
81
|
+
version: "0.1.0",
|
|
82
|
+
schemas: defineSchema({
|
|
83
|
+
[table]: {
|
|
84
|
+
fields: {
|
|
85
|
+
id: field.string,
|
|
86
|
+
resourceId: field.string,
|
|
87
|
+
parentCommentId: (value) => value === null || typeof value === "string",
|
|
88
|
+
authorId: field.string,
|
|
89
|
+
body: field.string,
|
|
90
|
+
depth: field.number,
|
|
91
|
+
createdAt: field.number,
|
|
92
|
+
editedAt: (value) => value === null || typeof value === "number"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
readers: { [table]: store.reader },
|
|
97
|
+
writers: { [table]: store.writer },
|
|
98
|
+
permissions: {
|
|
99
|
+
[table]: {
|
|
100
|
+
read: (ctx, row) => canReadResource(row.resourceId, ctx),
|
|
101
|
+
insert: (ctx, row) => {
|
|
102
|
+
const callerId = getActorId(ctx);
|
|
103
|
+
return callerId !== undefined && row.authorId === callerId;
|
|
104
|
+
},
|
|
105
|
+
update: (ctx, row) => {
|
|
106
|
+
const callerId = getActorId(ctx);
|
|
107
|
+
return callerId !== undefined && row.authorId === callerId;
|
|
108
|
+
},
|
|
109
|
+
delete: (ctx, row) => {
|
|
110
|
+
const callerId = getActorId(ctx);
|
|
111
|
+
if (callerId === undefined)
|
|
112
|
+
return false;
|
|
113
|
+
const authorId = row.authorId ?? (() => {
|
|
114
|
+
const id = row.id;
|
|
115
|
+
return id === undefined ? undefined : store.getById(id)?.authorId;
|
|
116
|
+
})();
|
|
117
|
+
if (authorId === undefined)
|
|
118
|
+
return false;
|
|
119
|
+
if (authorId === callerId)
|
|
120
|
+
return true;
|
|
121
|
+
return canModerate(ctx);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
collections: [
|
|
126
|
+
defineCollection({
|
|
127
|
+
name: collectionName,
|
|
128
|
+
tables: [table],
|
|
129
|
+
key: (row) => row.id,
|
|
130
|
+
hydrate: (params, ctx) => store.reader.all(ctx).filter((row) => row.resourceId === params.resourceId && canReadResource(row.resourceId, ctx)),
|
|
131
|
+
match: (row, params, ctx) => row.resourceId === params.resourceId && canReadResource(row.resourceId, ctx),
|
|
132
|
+
authorize: (params, ctx) => canReadResource(params.resourceId, ctx)
|
|
133
|
+
})
|
|
134
|
+
],
|
|
135
|
+
mutations: [
|
|
136
|
+
defineMutation({
|
|
137
|
+
name: createMutationName,
|
|
138
|
+
handler: async (args, ctx, actions) => {
|
|
139
|
+
if (!canReadResource(args.resourceId, ctx)) {
|
|
140
|
+
throw new UnauthorizedError(`comments:create on resource "${args.resourceId}"`);
|
|
141
|
+
}
|
|
142
|
+
const actorId = resolveActor(getActorId, ctx);
|
|
143
|
+
let depth = 0;
|
|
144
|
+
if (args.parentCommentId !== undefined && args.parentCommentId !== null) {
|
|
145
|
+
const parent = store.getById(args.parentCommentId);
|
|
146
|
+
if (parent === undefined || parent.resourceId !== args.resourceId) {
|
|
147
|
+
throw new CommentParentMismatchError(args.parentCommentId);
|
|
148
|
+
}
|
|
149
|
+
depth = parent.depth + 1;
|
|
150
|
+
if (depth > maxDepth) {
|
|
151
|
+
throw new CommentDepthExceededError(maxDepth, depth);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const row = {
|
|
155
|
+
id: newId(),
|
|
156
|
+
resourceId: args.resourceId,
|
|
157
|
+
parentCommentId: args.parentCommentId ?? null,
|
|
158
|
+
authorId: actorId,
|
|
159
|
+
body: args.body,
|
|
160
|
+
depth,
|
|
161
|
+
createdAt: now(),
|
|
162
|
+
editedAt: null
|
|
163
|
+
};
|
|
164
|
+
return await actions.insert(table, row);
|
|
165
|
+
}
|
|
166
|
+
}),
|
|
167
|
+
defineMutation({
|
|
168
|
+
name: editMutationName,
|
|
169
|
+
handler: async (args, ctx, actions) => {
|
|
170
|
+
const actorId = resolveActor(getActorId, ctx);
|
|
171
|
+
const existing = store.getById(args.commentId);
|
|
172
|
+
if (existing === undefined) {
|
|
173
|
+
throw new UnauthorizedError(`comments:edit on missing comment "${args.commentId}"`);
|
|
174
|
+
}
|
|
175
|
+
if (existing.authorId !== actorId) {
|
|
176
|
+
throw new UnauthorizedError(`comments:edit on "${args.commentId}" (not author)`);
|
|
177
|
+
}
|
|
178
|
+
const updated = {
|
|
179
|
+
...existing,
|
|
180
|
+
body: args.body,
|
|
181
|
+
editedAt: now()
|
|
182
|
+
};
|
|
183
|
+
return await actions.update(table, updated);
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
defineMutation({
|
|
187
|
+
name: deleteMutationName,
|
|
188
|
+
handler: async (args, ctx, actions) => {
|
|
189
|
+
const actorId = resolveActor(getActorId, ctx);
|
|
190
|
+
const existing = store.getById(args.commentId);
|
|
191
|
+
if (existing === undefined) {
|
|
192
|
+
throw new UnauthorizedError(`comments:delete on missing comment "${args.commentId}"`);
|
|
193
|
+
}
|
|
194
|
+
const isAuthor = existing.authorId === actorId;
|
|
195
|
+
const isModerator = canModerate(ctx);
|
|
196
|
+
if (!isAuthor && !isModerator) {
|
|
197
|
+
throw new UnauthorizedError(`comments:delete on "${args.commentId}" (not author, not moderator)`);
|
|
198
|
+
}
|
|
199
|
+
await actions.delete(table, { id: args.commentId });
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
]
|
|
203
|
+
};
|
|
204
|
+
if (config.bodyCrdt !== undefined) {
|
|
205
|
+
pack.crdt = { [table]: { body: config.bodyCrdt } };
|
|
206
|
+
}
|
|
207
|
+
return defineSyncPack(pack);
|
|
208
|
+
};
|
|
209
|
+
export {
|
|
210
|
+
createInMemoryCommentsStore,
|
|
211
|
+
createCommentsPack,
|
|
212
|
+
CommentParentMismatchError,
|
|
213
|
+
CommentDepthExceededError
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
//# debugId=435C25F77227D84464756E2164756E21
|
|
217
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * `@absolutejs/sync-pack-comments` — threaded comments on host-side resources\n * as a sync pack.\n *\n * Owns a single `comments` table; reads no host tables of its own (the join\n * to the host's user table is the pack's planned 0.2 feature — for now the\n * `comments` collection emits `authorId` and the client looks up display\n * info). Per-resource read access is gated through host-injected\n * `canReadResource(resourceId, ctx)`. Edits are author-only; deletes are\n * author or moderator. Optional `bodyCrdt` config wires the body field\n * through `registerCrdt` for conflict-free editing.\n *\n * @example\n * ```ts\n * import { createSyncEngine } from '@absolutejs/sync/engine';\n * import { createCommentsPack } from '@absolutejs/sync-pack-comments';\n *\n * const engine = createSyncEngine();\n * engine.registerPack(createCommentsPack({\n * getActorId: (ctx) => ctx.session.userId,\n * canReadResource: (resourceId, ctx) =>\n * hostAcl.canRead(resourceId, ctx.session.userId),\n * maxDepth: 8,\n * }));\n * ```\n */\n\nimport {\n\tdefineCollection,\n\tdefineMutation,\n\tdefineSchema,\n\tdefineSyncPack,\n\tfield,\n\tUnauthorizedError,\n\ttype CollectionContext,\n\ttype CrdtMergeable,\n\ttype SyncPack,\n\ttype TableReader,\n\ttype TableWriter\n} from '@absolutejs/sync/engine';\n\n/** A comment row stored in the pack's owned table. */\nexport type CommentRow = {\n\tid: string;\n\tresourceId: string;\n\t/** Null on top-level comments, the parent's id on replies. */\n\tparentCommentId: string | null;\n\tauthorId: string;\n\tbody: string;\n\t/** Depth in the thread (0 for top-level, parent.depth + 1 for replies). */\n\tdepth: number;\n\tcreatedAt: number;\n\t/** Null until the comment is edited; otherwise the timestamp of the most\n\t * recent edit. */\n\teditedAt: number | null;\n};\n\n/** Storage adapter the pack uses for the comments table. */\nexport type CommentsStore = {\n\treader: TableReader<CollectionContext>;\n\twriter: TableWriter<CommentRow, CollectionContext, unknown>;\n\t/** Lookup by row id; used by edit + delete mutations to verify ownership\n\t * and by create to walk the parent chain for depth math. */\n\tgetById: (id: string) => CommentRow | undefined;\n};\n\n/** Default in-memory store. Each pack instance creates its own. */\nexport const createInMemoryCommentsStore = (): CommentsStore => {\n\tconst rows = new Map<string, CommentRow>();\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 CommentRow;\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\n/** Thrown when a reply would exceed the configured `maxDepth`. */\nexport class CommentDepthExceededError extends Error {\n\treadonly maxDepth: number;\n\treadonly attemptedDepth: number;\n\tconstructor(maxDepth: number, attemptedDepth: number) {\n\t\tsuper(\n\t\t\t`Comment reply rejected: depth ${attemptedDepth} exceeds maxDepth ${maxDepth}`\n\t\t);\n\t\tthis.name = 'CommentDepthExceededError';\n\t\tthis.maxDepth = maxDepth;\n\t\tthis.attemptedDepth = attemptedDepth;\n\t}\n}\n\n/** Thrown when the requested parent comment doesn't exist or doesn't match. */\nexport class CommentParentMismatchError extends Error {\n\treadonly parentCommentId: string;\n\tconstructor(parentCommentId: string) {\n\t\tsuper(\n\t\t\t`Parent comment ${parentCommentId} not found, or its resourceId doesn't match the reply's resourceId`\n\t\t);\n\t\tthis.name = 'CommentParentMismatchError';\n\t\tthis.parentCommentId = parentCommentId;\n\t}\n}\n\n/** Factory config. `canReadResource` is the only required field. */\nexport type CommentsPackConfig<Ctx = CollectionContext> = {\n\t/**\n\t * Prefix applied to the owned table name AND every collection/mutation\n\t * the pack exposes. Default `\"\"`. Use a non-empty prefix when\n\t * registering multiple comments packs on one engine (e.g. one per\n\t * product surface).\n\t */\n\tprefix?: string;\n\t/**\n\t * Extract the current actor id from the app's ctx. Default:\n\t * `(ctx) => ctx.userId`. **This is the only contract the pack assumes\n\t * about the app's ctx**, alongside `canReadResource` / `canModerate`.\n\t */\n\tgetActorId?: (ctx: Ctx) => string | undefined;\n\t/**\n\t * Gate read access on a resource. The host knows which resources a\n\t * given ctx can read — the pack does NOT duplicate the host's ACL.\n\t * Required: pack reads return zero rows otherwise.\n\t */\n\tcanReadResource: (resourceId: string, ctx: Ctx) => boolean;\n\t/**\n\t * Optional moderation predicate. Used by `comments:delete` — author OR\n\t * moderator can delete. Default `() => false` (only authors delete).\n\t */\n\tcanModerate?: (ctx: Ctx) => boolean;\n\t/**\n\t * Maximum thread depth (top-level = 0, first reply = 1, ...). Replies\n\t * past this depth reject with {@link CommentDepthExceededError}.\n\t * Default `8`.\n\t */\n\tmaxDepth?: number;\n\t/**\n\t * When provided, register the comment row's `body` field as a CRDT via\n\t * `engine.registerCrdt`. The engine then merges concurrent edits\n\t * instead of overwriting. Typically `yjsText` from\n\t * `@absolutejs/sync-yjs` — the pack does NOT import Yjs; the host\n\t * provides the mergeable.\n\t */\n\tbodyCrdt?: CrdtMergeable<unknown>;\n\t/**\n\t * Override the storage adapter. Default: a per-instance in-memory\n\t * store. Pass your own for a persistent backend (Drizzle, Postgres,\n\t * Redis, ...).\n\t */\n\tstore?: CommentsStore;\n\t/** Wall-clock function. Default `Date.now`. */\n\tnow?: () => number;\n\t/**\n\t * Generate a fresh row id. Default `crypto.randomUUID()`. Override for\n\t * deterministic ids in tests, or to route through your existing id\n\t * scheme.\n\t */\n\tnewId?: () => string;\n};\n\nconst DEFAULT_MAX_DEPTH = 8;\n\nconst resolveActor = (\n\tgetActorId: NonNullable<CommentsPackConfig['getActorId']>,\n\tctx: unknown\n): string => {\n\tconst actorId = getActorId(ctx as CollectionContext);\n\tif (actorId === undefined || actorId === '') {\n\t\tthrow new UnauthorizedError('comments mutation (no actor id)');\n\t}\n\treturn actorId;\n};\n\n/**\n * Build a {@link SyncPack} that exposes threaded comments. Each call returns\n * a fresh pack with its own store (unless a custom `store` is supplied).\n */\nexport const createCommentsPack = <Ctx = CollectionContext>(\n\tconfig: CommentsPackConfig<Ctx>\n): SyncPack => {\n\tconst prefix = config.prefix ?? '';\n\tconst table = `${prefix}comments`;\n\tconst collectionName = table;\n\tconst createMutationName = `${prefix}comments:create`;\n\tconst editMutationName = `${prefix}comments:edit`;\n\tconst deleteMutationName = `${prefix}comments:delete`;\n\tconst maxDepth = config.maxDepth ?? DEFAULT_MAX_DEPTH;\n\tconst store = config.store ?? createInMemoryCommentsStore();\n\tconst now = config.now ?? Date.now;\n\tconst newId =\n\t\tconfig.newId ?? (() => globalThis.crypto.randomUUID());\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 canReadResource = config.canReadResource as (\n\t\tresourceId: string,\n\t\tctx: CollectionContext\n\t) => boolean;\n\tconst canModerate = (config.canModerate ?? (() => false)) as (\n\t\tctx: CollectionContext\n\t) => boolean;\n\n\ttype Params = { resourceId: string };\n\n\tconst pack: SyncPack = {\n\t\tname: '@absolutejs/sync-pack-comments',\n\t\townsTables: [table],\n\t\treadsTables: [],\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\tresourceId: field.string,\n\t\t\t\t\tparentCommentId: (value) =>\n\t\t\t\t\t\tvalue === null || typeof value === 'string',\n\t\t\t\t\tauthorId: field.string,\n\t\t\t\t\tbody: field.string,\n\t\t\t\t\tdepth: field.number,\n\t\t\t\t\tcreatedAt: field.number,\n\t\t\t\t\teditedAt: (value) =>\n\t\t\t\t\t\tvalue === null || typeof value === '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: CommentRow) =>\n\t\t\t\t\tcanReadResource(row.resourceId, ctx as CollectionContext),\n\t\t\t\t// The pack's own create handler stamps `authorId` from the\n\t\t\t\t// resolved actor, so this check passes inside the pack.\n\t\t\t\t// A host-side `actions.insert(\"comments\", { ... })` that\n\t\t\t\t// tried to forge an `authorId` would be rejected here.\n\t\t\t\tinsert: (ctx: unknown, row: CommentRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\treturn callerId !== undefined && row.authorId === callerId;\n\t\t\t\t},\n\t\t\t\t// Edits: author only. Same row-level check as insert.\n\t\t\t\tupdate: (ctx: unknown, row: CommentRow) => {\n\t\t\t\t\tconst callerId = getActorId(ctx as CollectionContext);\n\t\t\t\t\treturn callerId !== undefined && row.authorId === callerId;\n\t\t\t\t},\n\t\t\t\t// Deletes: author OR moderator. `actions.delete` may pass\n\t\t\t\t// the full row or just the key; if `authorId` isn't in the\n\t\t\t\t// supplied subject we look it up from the store.\n\t\t\t\tdelete: (ctx: unknown, row: CommentRow) => {\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 authorId =\n\t\t\t\t\t\t(row as { authorId?: string }).authorId ??\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)?.authorId;\n\t\t\t\t\t\t})();\n\t\t\t\t\tif (authorId === undefined) return false;\n\t\t\t\t\tif (authorId === callerId) return true;\n\t\t\t\t\treturn canModerate(ctx as CollectionContext);\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tcollections: [\n\t\t\tdefineCollection<CommentRow, 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\t(store.reader.all(ctx) as CommentRow[]).filter(\n\t\t\t\t\t\t(row) =>\n\t\t\t\t\t\t\trow.resourceId === params.resourceId &&\n\t\t\t\t\t\t\tcanReadResource(row.resourceId, ctx)\n\t\t\t\t\t),\n\t\t\t\tmatch: (row, params, ctx) =>\n\t\t\t\t\trow.resourceId === params.resourceId &&\n\t\t\t\t\tcanReadResource(row.resourceId, ctx),\n\t\t\t\tauthorize: (params, ctx) =>\n\t\t\t\t\tcanReadResource(params.resourceId, ctx)\n\t\t\t})\n\t\t],\n\n\t\tmutations: [\n\t\t\tdefineMutation<\n\t\t\t\t{\n\t\t\t\t\tresourceId: string;\n\t\t\t\t\tbody: string;\n\t\t\t\t\tparentCommentId?: string | null;\n\t\t\t\t},\n\t\t\t\tCollectionContext,\n\t\t\t\tCommentRow\n\t\t\t>({\n\t\t\t\tname: createMutationName,\n\t\t\t\thandler: async (args, ctx, actions) => {\n\t\t\t\t\tif (!canReadResource(args.resourceId, ctx)) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t`comments:create on resource \"${args.resourceId}\"`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst actorId = resolveActor(getActorId, ctx);\n\t\t\t\t\tlet depth = 0;\n\t\t\t\t\tif (\n\t\t\t\t\t\targs.parentCommentId !== undefined &&\n\t\t\t\t\t\targs.parentCommentId !== null\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst parent = store.getById(args.parentCommentId);\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tparent === undefined ||\n\t\t\t\t\t\t\tparent.resourceId !== args.resourceId\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthrow new CommentParentMismatchError(\n\t\t\t\t\t\t\t\targs.parentCommentId\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdepth = parent.depth + 1;\n\t\t\t\t\t\tif (depth > maxDepth) {\n\t\t\t\t\t\t\tthrow new CommentDepthExceededError(\n\t\t\t\t\t\t\t\tmaxDepth,\n\t\t\t\t\t\t\t\tdepth\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst row: CommentRow = {\n\t\t\t\t\t\tid: newId(),\n\t\t\t\t\t\tresourceId: args.resourceId,\n\t\t\t\t\t\tparentCommentId: args.parentCommentId ?? null,\n\t\t\t\t\t\tauthorId: actorId,\n\t\t\t\t\t\tbody: args.body,\n\t\t\t\t\t\tdepth,\n\t\t\t\t\t\tcreatedAt: now(),\n\t\t\t\t\t\teditedAt: null\n\t\t\t\t\t};\n\t\t\t\t\treturn (await actions.insert(table, row)) as CommentRow;\n\t\t\t\t}\n\t\t\t}),\n\t\t\tdefineMutation<\n\t\t\t\t{ commentId: string; body: string },\n\t\t\t\tCollectionContext,\n\t\t\t\tCommentRow\n\t\t\t>({\n\t\t\t\tname: editMutationName,\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 existing = store.getById(args.commentId);\n\t\t\t\t\tif (existing === undefined) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t`comments:edit on missing comment \"${args.commentId}\"`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif (existing.authorId !== actorId) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t`comments:edit on \"${args.commentId}\" (not author)`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst updated: CommentRow = {\n\t\t\t\t\t\t...existing,\n\t\t\t\t\t\tbody: args.body,\n\t\t\t\t\t\teditedAt: now()\n\t\t\t\t\t};\n\t\t\t\t\treturn (await actions.update(table, updated)) as CommentRow;\n\t\t\t\t}\n\t\t\t}),\n\t\t\tdefineMutation<{ commentId: string }, CollectionContext, void>({\n\t\t\t\tname: deleteMutationName,\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 existing = store.getById(args.commentId);\n\t\t\t\t\tif (existing === undefined) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t`comments:delete on missing comment \"${args.commentId}\"`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst isAuthor = existing.authorId === actorId;\n\t\t\t\t\tconst isModerator = canModerate(ctx);\n\t\t\t\t\tif (!isAuthor && !isModerator) {\n\t\t\t\t\t\tthrow new UnauthorizedError(\n\t\t\t\t\t\t\t`comments:delete on \"${args.commentId}\" (not author, not moderator)`\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tawait actions.delete(table, { id: args.commentId });\n\t\t\t\t}\n\t\t\t})\n\t\t]\n\t};\n\n\tif (config.bodyCrdt !== undefined) {\n\t\tpack.crdt = { [table]: { body: config.bodyCrdt } };\n\t}\n\n\treturn defineSyncPack(pack);\n};\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "435C25F77227D84464756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/sync-pack-comments",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Threaded comments pack for @absolutejs/sync — per-resource ACL-scoped, author/moderator gates, optional CRDT bodies",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/absolutejs/sync-packs.git",
|
|
8
|
+
"directory": "comments"
|
|
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
|
+
"comments",
|
|
24
|
+
"threading",
|
|
25
|
+
"collaboration"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "rm -rf dist && bun build src/index.ts --outdir dist --sourcemap --target=bun --external @absolutejs/sync && tsc --project tsconfig.build.json",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"format": "prettier --write \"./**/*.{ts,json,md}\"",
|
|
32
|
+
"release": "bun run format && bun run typecheck && bun run test && bun run build && bun publish"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@absolutejs/sync": ">= 1.9.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@absolutejs/sync": "^1.9.1",
|
|
39
|
+
"@types/bun": "1.3.14",
|
|
40
|
+
"prettier": "3.5.3",
|
|
41
|
+
"typescript": "5.8.3"
|
|
42
|
+
},
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"files": ["dist", "README.md"]
|
|
51
|
+
}
|