@feedclip/activity 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/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/ActivityPanel.d.ts +68 -0
- package/dist/ActivityPanel.d.ts.map +1 -0
- package/dist/activity.d.ts +118 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/adapters/memory.cjs +152 -0
- package/dist/adapters/memory.cjs.map +7 -0
- package/dist/adapters/memory.d.ts +2 -0
- package/dist/adapters/memory.d.ts.map +1 -0
- package/dist/adapters/memory.js +131 -0
- package/dist/adapters/memory.js.map +7 -0
- package/dist/adapters/postgres.cjs +287 -0
- package/dist/adapters/postgres.cjs.map +7 -0
- package/dist/adapters/postgres.d.ts +2 -0
- package/dist/adapters/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres.js +266 -0
- package/dist/adapters/postgres.js.map +7 -0
- package/dist/index.cjs +249 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +228 -0
- package/dist/index.js.map +7 -0
- package/dist/react.cjs +476 -0
- package/dist/react.cjs.map +7 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +455 -0
- package/dist/react.js.map +7 -0
- package/dist/styles.css +1 -0
- package/migrations/001_activity_schema.sql +44 -0
- package/package.json +86 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/activity.ts
|
|
6
|
+
var ActivityError = class extends Error {
|
|
7
|
+
constructor(code, message, field) {
|
|
8
|
+
super(message);
|
|
9
|
+
__publicField(this, "code");
|
|
10
|
+
__publicField(this, "field");
|
|
11
|
+
this.name = "ActivityError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.field = field;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function createActivity({
|
|
17
|
+
adapter,
|
|
18
|
+
clock = () => /* @__PURE__ */ new Date(),
|
|
19
|
+
idGenerator = createId
|
|
20
|
+
}) {
|
|
21
|
+
return {
|
|
22
|
+
async track(input) {
|
|
23
|
+
const record = createRecord(input, clock, idGenerator);
|
|
24
|
+
await adapter.insert(record);
|
|
25
|
+
return record;
|
|
26
|
+
},
|
|
27
|
+
async query(options) {
|
|
28
|
+
const normalized = normalizeQuery(options);
|
|
29
|
+
const result = await adapter.query(normalized);
|
|
30
|
+
return result.entries;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function createMemoryStorageAdapter(initialEntries = [], latency = 0) {
|
|
35
|
+
let entries = initialEntries.map(freezeRecord);
|
|
36
|
+
return {
|
|
37
|
+
async insert(entry) {
|
|
38
|
+
await wait(latency);
|
|
39
|
+
entries = [freezeRecord(entry), ...entries];
|
|
40
|
+
},
|
|
41
|
+
async query(options) {
|
|
42
|
+
await wait(latency);
|
|
43
|
+
const normalized = normalizeQuery(options);
|
|
44
|
+
const filtered = entries.filter((entry) => entry.resource.type === normalized.resource.type).filter((entry) => entry.resource.id === normalized.resource.id).filter((entry) => matchesActions(entry, normalized.actions)).filter((entry) => matchesActor(entry, normalized.actorId ?? normalized.actor)).filter((entry) => matchesDates(entry, normalized.from, normalized.to)).filter((entry) => matchesSearch(entry, normalized.search)).sort(compareRecords);
|
|
45
|
+
const offset = normalized.offset ?? 0;
|
|
46
|
+
const limit = normalized.limit ?? 50;
|
|
47
|
+
const page = filtered.slice(offset, offset + limit);
|
|
48
|
+
return {
|
|
49
|
+
entries: page,
|
|
50
|
+
total: filtered.length,
|
|
51
|
+
hasMore: offset + page.length < filtered.length
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createRecord(input, clock, idGenerator) {
|
|
57
|
+
validateTrackInput(input);
|
|
58
|
+
const record = {
|
|
59
|
+
id: idGenerator(),
|
|
60
|
+
resource: freezeObject({
|
|
61
|
+
type: input.resource.type.trim(),
|
|
62
|
+
id: input.resource.id.trim(),
|
|
63
|
+
title: input.resource.title?.trim()
|
|
64
|
+
}),
|
|
65
|
+
action: input.action.trim().toLowerCase(),
|
|
66
|
+
actor: freezeObject({ ...input.actor, name: input.actor.name.trim() }),
|
|
67
|
+
timestamp: input.timestamp ? new Date(input.timestamp) : clock(),
|
|
68
|
+
changes: input.changes?.map(normalizeChange).map(freezeObject),
|
|
69
|
+
content: input.content ? freezeObject(input.content) : void 0,
|
|
70
|
+
metadata: input.metadata ? freezeObject(input.metadata) : void 0
|
|
71
|
+
};
|
|
72
|
+
return freezeRecord(record);
|
|
73
|
+
}
|
|
74
|
+
function validateTrackInput(input) {
|
|
75
|
+
if (!input.resource?.type?.trim()) {
|
|
76
|
+
throw new ActivityError("INVALID_RESOURCE", "resource.type is required", "resource.type");
|
|
77
|
+
}
|
|
78
|
+
if (!input.resource?.id?.trim()) {
|
|
79
|
+
throw new ActivityError("INVALID_RESOURCE", "resource.id is required", "resource.id");
|
|
80
|
+
}
|
|
81
|
+
if (!["user", "system", "api", "agent"].includes(input.actor?.type)) {
|
|
82
|
+
throw new ActivityError("INVALID_ACTOR", "actor.type is invalid", "actor.type");
|
|
83
|
+
}
|
|
84
|
+
if (!input.actor?.id?.trim()) {
|
|
85
|
+
throw new ActivityError("INVALID_ACTOR", "actor.id is required", "actor.id");
|
|
86
|
+
}
|
|
87
|
+
if (!input.actor?.name?.trim()) {
|
|
88
|
+
throw new ActivityError("INVALID_ACTOR", "actor.name is required", "actor.name");
|
|
89
|
+
}
|
|
90
|
+
if (!input.action?.trim()) {
|
|
91
|
+
throw new ActivityError("INVALID_ACTION", "action is required", "action");
|
|
92
|
+
}
|
|
93
|
+
if (input.timestamp && Number.isNaN(input.timestamp.getTime())) {
|
|
94
|
+
throw new ActivityError("INVALID_TIMESTAMP", "timestamp must be a valid Date", "timestamp");
|
|
95
|
+
}
|
|
96
|
+
if (input.action === "update" && !input.changes?.length) {
|
|
97
|
+
throw new ActivityError("INVALID_ACTION", "update requires at least one change", "changes");
|
|
98
|
+
}
|
|
99
|
+
if ((input.action === "comment" || input.action === "attachment") && !input.content) {
|
|
100
|
+
throw new ActivityError("INVALID_ACTION", `${input.action} requires content`, "content");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function normalizeChange(change) {
|
|
104
|
+
if (!change.field.trim()) {
|
|
105
|
+
throw new ActivityError("INVALID_CHANGE", "change.field is required", "changes.field");
|
|
106
|
+
}
|
|
107
|
+
if (!change.label.trim()) {
|
|
108
|
+
throw new ActivityError("INVALID_CHANGE", "change.label is required", "changes.label");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...change,
|
|
112
|
+
field: change.field.trim(),
|
|
113
|
+
label: change.label.trim(),
|
|
114
|
+
valueType: change.valueType ?? inferValueType(change.after ?? change.before)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function normalizeQuery(options) {
|
|
118
|
+
if (!options.resource?.type?.trim() || !options.resource?.id?.trim()) {
|
|
119
|
+
throw new ActivityError("INVALID_RESOURCE", "query.resource is required", "resource");
|
|
120
|
+
}
|
|
121
|
+
if (options.limit !== void 0 && (options.limit < 1 || options.limit > 500)) {
|
|
122
|
+
throw new ActivityError("INVALID_LIMIT", "limit must be between 1 and 500", "limit");
|
|
123
|
+
}
|
|
124
|
+
if (options.offset !== void 0 && options.offset < 0) {
|
|
125
|
+
throw new ActivityError("INVALID_OFFSET", "offset must not be negative", "offset");
|
|
126
|
+
}
|
|
127
|
+
if (options.from && options.to && options.from > options.to) {
|
|
128
|
+
throw new ActivityError("INVALID_DATE_RANGE", "from must be before to", "from");
|
|
129
|
+
}
|
|
130
|
+
return freezeObject({
|
|
131
|
+
...options,
|
|
132
|
+
resource: freezeObject({
|
|
133
|
+
type: options.resource.type.trim(),
|
|
134
|
+
id: options.resource.id.trim(),
|
|
135
|
+
title: options.resource.title?.trim()
|
|
136
|
+
}),
|
|
137
|
+
search: options.search?.trim() || void 0,
|
|
138
|
+
actor: options.actor?.trim() || void 0,
|
|
139
|
+
actorId: options.actorId?.trim() || void 0,
|
|
140
|
+
actions: options.actions ? Object.freeze(
|
|
141
|
+
Array.from(
|
|
142
|
+
new Set(options.actions.map((action) => action.trim().toLowerCase()))
|
|
143
|
+
)
|
|
144
|
+
) : void 0,
|
|
145
|
+
limit: options.limit ?? 50,
|
|
146
|
+
offset: options.offset ?? 0
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function matchesActions(entry, actions) {
|
|
150
|
+
return !actions?.length || actions.includes(entry.action);
|
|
151
|
+
}
|
|
152
|
+
function matchesActor(entry, actorId) {
|
|
153
|
+
return !actorId || entry.actor.id === actorId;
|
|
154
|
+
}
|
|
155
|
+
function matchesDates(entry, from, to) {
|
|
156
|
+
return (!from || entry.timestamp >= from) && (!to || entry.timestamp <= to);
|
|
157
|
+
}
|
|
158
|
+
function matchesSearch(entry, search) {
|
|
159
|
+
if (!search) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return searchableText(entry).toLowerCase().includes(search.toLowerCase());
|
|
163
|
+
}
|
|
164
|
+
function searchableText(entry) {
|
|
165
|
+
return [
|
|
166
|
+
entry.action,
|
|
167
|
+
entry.resource.title,
|
|
168
|
+
entry.actor.name,
|
|
169
|
+
entry.content ? JSON.stringify(entry.content) : "",
|
|
170
|
+
...(entry.changes ?? []).flatMap((change) => [
|
|
171
|
+
change.field,
|
|
172
|
+
change.label,
|
|
173
|
+
stringifyValue(change.before),
|
|
174
|
+
stringifyValue(change.after)
|
|
175
|
+
])
|
|
176
|
+
].join(" ");
|
|
177
|
+
}
|
|
178
|
+
function compareRecords(a, b) {
|
|
179
|
+
const time = b.timestamp.getTime() - a.timestamp.getTime();
|
|
180
|
+
return time || b.id.localeCompare(a.id);
|
|
181
|
+
}
|
|
182
|
+
function inferValueType(value) {
|
|
183
|
+
if (value instanceof Date) {
|
|
184
|
+
return "date";
|
|
185
|
+
}
|
|
186
|
+
const valueType = typeof value;
|
|
187
|
+
if (valueType === "string" || valueType === "number" || valueType === "boolean") {
|
|
188
|
+
return valueType;
|
|
189
|
+
}
|
|
190
|
+
return "json";
|
|
191
|
+
}
|
|
192
|
+
function freezeRecord(record) {
|
|
193
|
+
return freezeObject({
|
|
194
|
+
...record,
|
|
195
|
+
resource: freezeObject(record.resource),
|
|
196
|
+
actor: freezeObject(record.actor),
|
|
197
|
+
changes: record.changes?.map((change) => freezeObject(change)),
|
|
198
|
+
content: record.content ? freezeObject(record.content) : void 0,
|
|
199
|
+
metadata: record.metadata ? freezeObject(record.metadata) : void 0
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function freezeObject(value) {
|
|
203
|
+
return Object.freeze(value);
|
|
204
|
+
}
|
|
205
|
+
function stringifyValue(value) {
|
|
206
|
+
if (value === null || value === void 0) {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
if (value instanceof Date) {
|
|
210
|
+
return value.toISOString();
|
|
211
|
+
}
|
|
212
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
213
|
+
}
|
|
214
|
+
function createId() {
|
|
215
|
+
if (globalThis.crypto?.randomUUID) {
|
|
216
|
+
return globalThis.crypto.randomUUID();
|
|
217
|
+
}
|
|
218
|
+
return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
219
|
+
}
|
|
220
|
+
function wait(ms) {
|
|
221
|
+
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
222
|
+
}
|
|
223
|
+
export {
|
|
224
|
+
ActivityError,
|
|
225
|
+
createActivity,
|
|
226
|
+
createMemoryStorageAdapter
|
|
227
|
+
};
|
|
228
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/activity.ts"],
|
|
4
|
+
"sourcesContent": ["export type ActorType = \"user\" | \"system\" | \"api\" | \"agent\";\n\nexport type BuiltInAction =\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"archive\"\n | \"restore\"\n | \"comment\"\n | \"attachment\";\n\nexport type Action = BuiltInAction | (string & {});\n\nexport type ValueType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"currency\"\n | \"user\"\n | \"enum\"\n | \"json\"\n | \"custom\";\n\nexport type Resource = {\n type: string;\n id: string;\n title?: string;\n};\n\nexport type ResourceReference = Pick<Resource, \"type\" | \"id\"> & {\n title?: string;\n};\n\nexport type Actor = {\n type: ActorType;\n id: string;\n name: string;\n avatarUrl?: string;\n};\n\nexport type Change = {\n field: string;\n label: string;\n before?: unknown;\n after?: unknown;\n valueType: ValueType;\n};\n\nexport type CommentContent = {\n type: \"comment\";\n text: string;\n};\n\nexport type AttachmentContent = {\n type: \"attachment\";\n fileName: string;\n mimeType: string;\n size: number;\n url?: string;\n};\n\nexport type CustomContent = {\n type: \"custom\";\n [key: string]: unknown;\n};\n\nexport type Content = CommentContent | AttachmentContent | CustomContent;\n\nexport type Metadata = {\n source?: string;\n version?: string;\n requestId?: string;\n ipAddress?: string;\n custom?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type ActivityRecord = Readonly<{\n id: string;\n resource: Readonly<Resource>;\n action: Action;\n actor: Readonly<Actor>;\n timestamp: Date;\n changes?: readonly Readonly<Change>[];\n content?: Readonly<Content>;\n metadata?: Readonly<Metadata>;\n}>;\n\nexport type TrackInput = {\n resource: ResourceReference;\n actor: Actor;\n action: Action;\n changes?: Change[];\n content?: Content;\n metadata?: Metadata;\n timestamp?: Date;\n};\n\nexport type QueryOptions = {\n resource: ResourceReference;\n search?: string;\n actor?: string;\n actorId?: string;\n actions?: readonly Action[];\n from?: Date;\n to?: Date;\n limit?: number;\n offset?: number;\n};\n\nexport type QueryResult = {\n entries: ActivityRecord[];\n total: number;\n hasMore: boolean;\n};\n\nexport type StorageAdapter = {\n insert(entry: ActivityRecord): Promise<void>;\n query(options: QueryOptions): Promise<QueryResult>;\n};\n\nexport type Activity = {\n track(input: TrackInput): Promise<ActivityRecord>;\n query(options: QueryOptions): Promise<ActivityRecord[]>;\n};\n\nexport type ActivityOptions = {\n adapter: StorageAdapter;\n clock?: () => Date;\n idGenerator?: () => string;\n};\n\nexport class ActivityError extends Error {\n readonly code: string;\n readonly field?: string;\n\n constructor(code: string, message: string, field?: string) {\n super(message);\n this.name = \"ActivityError\";\n this.code = code;\n this.field = field;\n }\n}\n\nexport function createActivity({\n adapter,\n clock = () => new Date(),\n idGenerator = createId,\n}: ActivityOptions): Activity {\n return {\n async track(input) {\n const record = createRecord(input, clock, idGenerator);\n await adapter.insert(record);\n return record;\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const result = await adapter.query(normalized);\n return result.entries;\n },\n };\n}\n\nexport function createMemoryStorageAdapter(\n initialEntries: ActivityRecord[] = [],\n latency = 0,\n): StorageAdapter {\n let entries = initialEntries.map(freezeRecord);\n\n return {\n async insert(entry) {\n await wait(latency);\n entries = [freezeRecord(entry), ...entries];\n },\n async query(options) {\n await wait(latency);\n const normalized = normalizeQuery(options);\n const filtered = entries\n .filter((entry) => entry.resource.type === normalized.resource.type)\n .filter((entry) => entry.resource.id === normalized.resource.id)\n .filter((entry) => matchesActions(entry, normalized.actions))\n .filter((entry) => matchesActor(entry, normalized.actorId ?? normalized.actor))\n .filter((entry) => matchesDates(entry, normalized.from, normalized.to))\n .filter((entry) => matchesSearch(entry, normalized.search))\n .sort(compareRecords);\n const offset = normalized.offset ?? 0;\n const limit = normalized.limit ?? 50;\n const page = filtered.slice(offset, offset + limit);\n\n return {\n entries: page,\n total: filtered.length,\n hasMore: offset + page.length < filtered.length,\n };\n },\n };\n}\n\ntype Queryable = {\n query(sql: string, params?: readonly unknown[]): Promise<{ rows: unknown[] }>;\n};\n\ntype ReleasableQueryable = Queryable & { release(): void };\ntype Connectable = Queryable & { connect(): Promise<ReleasableQueryable> };\n\nexport function postgresAdapter(client: Queryable | Connectable): StorageAdapter {\n return {\n async insert(entry) {\n const connection = \"connect\" in client ? await client.connect() : client;\n await connection.query(\"BEGIN\");\n try {\n await connection.query(\n `insert into activity_entries (\n id, resource_type, resource_id, resource_title, action,\n actor_type, actor_id, actor_name, actor_avatar_url,\n content_type, content_json, metadata_json, created_at\n ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,\n [\n entry.id,\n entry.resource.type,\n entry.resource.id,\n entry.resource.title ?? null,\n entry.action,\n entry.actor.type,\n entry.actor.id,\n entry.actor.name,\n entry.actor.avatarUrl ?? null,\n entry.content?.type ?? null,\n entry.content ? JSON.stringify(entry.content) : null,\n entry.metadata ? JSON.stringify(entry.metadata) : null,\n entry.timestamp.toISOString(),\n ],\n );\n\n for (const [position, change] of (entry.changes ?? []).entries()) {\n await connection.query(\n `insert into activity_changes (\n id, entry_id, position, field, label, before_value, after_value, value_type\n ) values ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [\n createId(),\n entry.id,\n position,\n change.field,\n change.label,\n JSON.stringify(change.before ?? null),\n JSON.stringify(change.after ?? null),\n change.valueType,\n ],\n );\n }\n\n await connection.query(\"COMMIT\");\n } catch (error) {\n await connection.query(\"ROLLBACK\");\n throw toActivityError(error, \"STORAGE_FAILURE\", \"Could not insert activity record\");\n } finally {\n if (\"release\" in connection && typeof connection.release === \"function\") {\n connection.release();\n }\n }\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const limit = normalized.limit ?? 50;\n const offset = normalized.offset ?? 0;\n const params: unknown[] = [normalized.resource.type, normalized.resource.id];\n const where = [\"e.resource_type = $1\", \"e.resource_id = $2\"];\n\n if (normalized.actions?.length) {\n params.push(normalized.actions);\n where.push(`e.action = any($${params.length}::text[])`);\n }\n\n const actorId = normalized.actorId ?? normalized.actor;\n if (actorId) {\n params.push(actorId);\n where.push(`e.actor_id = $${params.length}`);\n }\n\n if (normalized.from) {\n params.push(normalized.from.toISOString());\n where.push(`e.created_at >= $${params.length}`);\n }\n\n if (normalized.to) {\n params.push(normalized.to.toISOString());\n where.push(`e.created_at <= $${params.length}`);\n }\n\n if (normalized.search) {\n params.push(`%${normalized.search}%`);\n const index = params.length;\n where.push(`(\n e.actor_name ilike $${index}\n or e.resource_title ilike $${index}\n or e.content_json::text ilike $${index}\n or exists (\n select 1 from activity_changes c\n where c.entry_id = e.id\n and (\n c.label ilike $${index}\n or c.before_value::text ilike $${index}\n or c.after_value::text ilike $${index}\n )\n )\n )`);\n }\n\n const whereSql = where.join(\" and \");\n const count = await client.query(\n `select count(*)::int as total from activity_entries e where ${whereSql}`,\n params,\n );\n params.push(limit + 1, offset);\n const rows = await client.query(\n `select\n e.*,\n coalesce(\n json_agg(\n json_build_object(\n 'field', c.field,\n 'label', c.label,\n 'before', c.before_value,\n 'after', c.after_value,\n 'valueType', c.value_type\n )\n order by c.position\n ) filter (where c.id is not null),\n '[]'\n ) as changes\n from activity_entries e\n left join activity_changes c on c.entry_id = e.id\n where ${whereSql}\n group by e.id\n order by e.created_at desc, e.id desc\n limit $${params.length - 1} offset $${params.length}`,\n params,\n );\n\n const total = readTotal(count.rows[0]);\n const entries = rows.rows.slice(0, limit).map(mapPostgresRecord);\n\n return {\n entries,\n total,\n hasMore: rows.rows.length > limit,\n };\n },\n };\n}\n\nfunction createRecord(\n input: TrackInput,\n clock: () => Date,\n idGenerator: () => string,\n): ActivityRecord {\n validateTrackInput(input);\n\n const record: ActivityRecord = {\n id: idGenerator(),\n resource: freezeObject({\n type: input.resource.type.trim(),\n id: input.resource.id.trim(),\n title: input.resource.title?.trim(),\n }),\n action: input.action.trim().toLowerCase() as Action,\n actor: freezeObject({ ...input.actor, name: input.actor.name.trim() }),\n timestamp: input.timestamp ? new Date(input.timestamp) : clock(),\n changes: input.changes?.map(normalizeChange).map(freezeObject),\n content: input.content ? freezeObject(input.content) : undefined,\n metadata: input.metadata ? freezeObject(input.metadata) : undefined,\n };\n\n return freezeRecord(record);\n}\n\nfunction validateTrackInput(input: TrackInput) {\n if (!input.resource?.type?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.type is required\", \"resource.type\");\n }\n\n if (!input.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.id is required\", \"resource.id\");\n }\n\n if (![\"user\", \"system\", \"api\", \"agent\"].includes(input.actor?.type)) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.type is invalid\", \"actor.type\");\n }\n\n if (!input.actor?.id?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.id is required\", \"actor.id\");\n }\n\n if (!input.actor?.name?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.name is required\", \"actor.name\");\n }\n\n if (!input.action?.trim()) {\n throw new ActivityError(\"INVALID_ACTION\", \"action is required\", \"action\");\n }\n\n if (input.timestamp && Number.isNaN(input.timestamp.getTime())) {\n throw new ActivityError(\"INVALID_TIMESTAMP\", \"timestamp must be a valid Date\", \"timestamp\");\n }\n\n if (input.action === \"update\" && !input.changes?.length) {\n throw new ActivityError(\"INVALID_ACTION\", \"update requires at least one change\", \"changes\");\n }\n\n if ((input.action === \"comment\" || input.action === \"attachment\") && !input.content) {\n throw new ActivityError(\"INVALID_ACTION\", `${input.action} requires content`, \"content\");\n }\n}\n\nfunction normalizeChange(change: Change): Change {\n if (!change.field.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.field is required\", \"changes.field\");\n }\n\n if (!change.label.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.label is required\", \"changes.label\");\n }\n\n return {\n ...change,\n field: change.field.trim(),\n label: change.label.trim(),\n valueType: change.valueType ?? inferValueType(change.after ?? change.before),\n };\n}\n\nfunction normalizeQuery(options: QueryOptions): QueryOptions {\n if (!options.resource?.type?.trim() || !options.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"query.resource is required\", \"resource\");\n }\n\n if (options.limit !== undefined && (options.limit < 1 || options.limit > 500)) {\n throw new ActivityError(\"INVALID_LIMIT\", \"limit must be between 1 and 500\", \"limit\");\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n throw new ActivityError(\"INVALID_OFFSET\", \"offset must not be negative\", \"offset\");\n }\n\n if (options.from && options.to && options.from > options.to) {\n throw new ActivityError(\"INVALID_DATE_RANGE\", \"from must be before to\", \"from\");\n }\n\n return freezeObject({\n ...options,\n resource: freezeObject({\n type: options.resource.type.trim(),\n id: options.resource.id.trim(),\n title: options.resource.title?.trim(),\n }),\n search: options.search?.trim() || undefined,\n actor: options.actor?.trim() || undefined,\n actorId: options.actorId?.trim() || undefined,\n actions: options.actions\n ? Object.freeze(\n Array.from(\n new Set(options.actions.map((action) => action.trim().toLowerCase() as Action)),\n ),\n )\n : undefined,\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n });\n}\n\nfunction matchesActions(entry: ActivityRecord, actions?: readonly Action[]) {\n return !actions?.length || actions.includes(entry.action);\n}\n\nfunction matchesActor(entry: ActivityRecord, actorId?: string) {\n return !actorId || entry.actor.id === actorId;\n}\n\nfunction matchesDates(entry: ActivityRecord, from?: Date, to?: Date) {\n return (!from || entry.timestamp >= from) && (!to || entry.timestamp <= to);\n}\n\nfunction matchesSearch(entry: ActivityRecord, search?: string) {\n if (!search) {\n return true;\n }\n\n return searchableText(entry).toLowerCase().includes(search.toLowerCase());\n}\n\nfunction searchableText(entry: ActivityRecord) {\n return [\n entry.action,\n entry.resource.title,\n entry.actor.name,\n entry.content ? JSON.stringify(entry.content) : \"\",\n ...(entry.changes ?? []).flatMap((change) => [\n change.field,\n change.label,\n stringifyValue(change.before),\n stringifyValue(change.after),\n ]),\n ].join(\" \");\n}\n\nfunction compareRecords(a: ActivityRecord, b: ActivityRecord) {\n const time = b.timestamp.getTime() - a.timestamp.getTime();\n return time || b.id.localeCompare(a.id);\n}\n\nfunction inferValueType(value: unknown): ValueType {\n if (value instanceof Date) {\n return \"date\";\n }\n\n const valueType = typeof value;\n if (valueType === \"string\" || valueType === \"number\" || valueType === \"boolean\") {\n return valueType;\n }\n\n return \"json\";\n}\n\nfunction freezeRecord(record: ActivityRecord): ActivityRecord {\n return freezeObject({\n ...record,\n resource: freezeObject(record.resource),\n actor: freezeObject(record.actor),\n changes: record.changes?.map((change) => freezeObject(change)),\n content: record.content ? freezeObject(record.content) : undefined,\n metadata: record.metadata ? freezeObject(record.metadata) : undefined,\n });\n}\n\nfunction freezeObject<T extends object>(value: T): Readonly<T> {\n return Object.freeze(value);\n}\n\nfunction stringifyValue(value: unknown) {\n if (value === null || value === undefined) {\n return \"\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n\nfunction createId() {\n if (globalThis.crypto?.randomUUID) {\n return globalThis.crypto.randomUUID();\n }\n\n return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;\n}\n\nfunction wait(ms: number) {\n return new Promise((resolve) => globalThis.setTimeout(resolve, ms));\n}\n\nfunction readTotal(row: unknown) {\n if (isRecord(row) && typeof row.total === \"number\") {\n return row.total;\n }\n\n if (isRecord(row) && typeof row.total === \"string\") {\n return Number(row.total);\n }\n\n return 0;\n}\n\nfunction mapPostgresRecord(row: unknown): ActivityRecord {\n if (!isRecord(row)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid activity row returned by storage\");\n }\n\n return freezeRecord({\n id: String(row.id),\n resource: {\n type: String(row.resource_type),\n id: String(row.resource_id),\n title: typeof row.resource_title === \"string\" ? row.resource_title : undefined,\n },\n action: String(row.action) as Action,\n actor: {\n type: String(row.actor_type) as ActorType,\n id: String(row.actor_id),\n name: String(row.actor_name),\n avatarUrl: typeof row.actor_avatar_url === \"string\" ? row.actor_avatar_url : undefined,\n },\n timestamp: new Date(String(row.created_at)),\n changes: Array.isArray(row.changes) ? row.changes.map(mapPostgresChange) : undefined,\n content: isContent(row.content_json) ? row.content_json : undefined,\n metadata: isRecord(row.metadata_json) ? row.metadata_json : undefined,\n });\n}\n\nfunction mapPostgresChange(change: unknown): Change {\n if (!isRecord(change)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid change row returned by storage\");\n }\n\n return {\n field: String(change.field),\n label: String(change.label),\n before: change.before,\n after: change.after,\n valueType: String(change.valueType) as ValueType,\n };\n}\n\nfunction isContent(value: unknown): value is Content {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toActivityError(error: unknown, code: string, fallback: string) {\n if (error instanceof ActivityError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new ActivityError(code, error.message);\n }\n\n return new ActivityError(code, fallback);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;AAqIO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAIvC,YAAY,MAAc,SAAiB,OAAgB;AACzD,UAAM,OAAO;AAJf,wBAAS;AACT,wBAAS;AAIP,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;AAEO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA,QAAQ,MAAM,oBAAI,KAAK;AAAA,EACvB,cAAc;AAChB,GAA8B;AAC5B,SAAO;AAAA,IACL,MAAM,MAAM,OAAO;AACjB,YAAM,SAAS,aAAa,OAAO,OAAO,WAAW;AACrD,YAAM,QAAQ,OAAO,MAAM;AAC3B,aAAO;AAAA,IACT;AAAA,IACA,MAAM,MAAM,SAAS;AACnB,YAAM,aAAa,eAAe,OAAO;AACzC,YAAM,SAAS,MAAM,QAAQ,MAAM,UAAU;AAC7C,aAAO,OAAO;AAAA,IAChB;AAAA,EACF;AACF;AAEO,SAAS,2BACd,iBAAmC,CAAC,GACpC,UAAU,GACM;AAChB,MAAI,UAAU,eAAe,IAAI,YAAY;AAE7C,SAAO;AAAA,IACL,MAAM,OAAO,OAAO;AAClB,YAAM,KAAK,OAAO;AAClB,gBAAU,CAAC,aAAa,KAAK,GAAG,GAAG,OAAO;AAAA,IAC5C;AAAA,IACA,MAAM,MAAM,SAAS;AACnB,YAAM,KAAK,OAAO;AAClB,YAAM,aAAa,eAAe,OAAO;AACzC,YAAM,WAAW,QACd,OAAO,CAAC,UAAU,MAAM,SAAS,SAAS,WAAW,SAAS,IAAI,EAClE,OAAO,CAAC,UAAU,MAAM,SAAS,OAAO,WAAW,SAAS,EAAE,EAC9D,OAAO,CAAC,UAAU,eAAe,OAAO,WAAW,OAAO,CAAC,EAC3D,OAAO,CAAC,UAAU,aAAa,OAAO,WAAW,WAAW,WAAW,KAAK,CAAC,EAC7E,OAAO,CAAC,UAAU,aAAa,OAAO,WAAW,MAAM,WAAW,EAAE,CAAC,EACrE,OAAO,CAAC,UAAU,cAAc,OAAO,WAAW,MAAM,CAAC,EACzD,KAAK,cAAc;AACtB,YAAM,SAAS,WAAW,UAAU;AACpC,YAAM,QAAQ,WAAW,SAAS;AAClC,YAAM,OAAO,SAAS,MAAM,QAAQ,SAAS,KAAK;AAElD,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,SAAS;AAAA,QAChB,SAAS,SAAS,KAAK,SAAS,SAAS;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AACF;AA4JA,SAAS,aACP,OACA,OACA,aACgB;AAChB,qBAAmB,KAAK;AAExB,QAAM,SAAyB;AAAA,IAC7B,IAAI,YAAY;AAAA,IAChB,UAAU,aAAa;AAAA,MACrB,MAAM,MAAM,SAAS,KAAK,KAAK;AAAA,MAC/B,IAAI,MAAM,SAAS,GAAG,KAAK;AAAA,MAC3B,OAAO,MAAM,SAAS,OAAO,KAAK;AAAA,IACpC,CAAC;AAAA,IACD,QAAQ,MAAM,OAAO,KAAK,EAAE,YAAY;AAAA,IACxC,OAAO,aAAa,EAAE,GAAG,MAAM,OAAO,MAAM,MAAM,MAAM,KAAK,KAAK,EAAE,CAAC;AAAA,IACrE,WAAW,MAAM,YAAY,IAAI,KAAK,MAAM,SAAS,IAAI,MAAM;AAAA,IAC/D,SAAS,MAAM,SAAS,IAAI,eAAe,EAAE,IAAI,YAAY;AAAA,IAC7D,SAAS,MAAM,UAAU,aAAa,MAAM,OAAO,IAAI;AAAA,IACvD,UAAU,MAAM,WAAW,aAAa,MAAM,QAAQ,IAAI;AAAA,EAC5D;AAEA,SAAO,aAAa,MAAM;AAC5B;AAEA,SAAS,mBAAmB,OAAmB;AAC7C,MAAI,CAAC,MAAM,UAAU,MAAM,KAAK,GAAG;AACjC,UAAM,IAAI,cAAc,oBAAoB,6BAA6B,eAAe;AAAA,EAC1F;AAEA,MAAI,CAAC,MAAM,UAAU,IAAI,KAAK,GAAG;AAC/B,UAAM,IAAI,cAAc,oBAAoB,2BAA2B,aAAa;AAAA,EACtF;AAEA,MAAI,CAAC,CAAC,QAAQ,UAAU,OAAO,OAAO,EAAE,SAAS,MAAM,OAAO,IAAI,GAAG;AACnE,UAAM,IAAI,cAAc,iBAAiB,yBAAyB,YAAY;AAAA,EAChF;AAEA,MAAI,CAAC,MAAM,OAAO,IAAI,KAAK,GAAG;AAC5B,UAAM,IAAI,cAAc,iBAAiB,wBAAwB,UAAU;AAAA,EAC7E;AAEA,MAAI,CAAC,MAAM,OAAO,MAAM,KAAK,GAAG;AAC9B,UAAM,IAAI,cAAc,iBAAiB,0BAA0B,YAAY;AAAA,EACjF;AAEA,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,cAAc,kBAAkB,sBAAsB,QAAQ;AAAA,EAC1E;AAEA,MAAI,MAAM,aAAa,OAAO,MAAM,MAAM,UAAU,QAAQ,CAAC,GAAG;AAC9D,UAAM,IAAI,cAAc,qBAAqB,kCAAkC,WAAW;AAAA,EAC5F;AAEA,MAAI,MAAM,WAAW,YAAY,CAAC,MAAM,SAAS,QAAQ;AACvD,UAAM,IAAI,cAAc,kBAAkB,uCAAuC,SAAS;AAAA,EAC5F;AAEA,OAAK,MAAM,WAAW,aAAa,MAAM,WAAW,iBAAiB,CAAC,MAAM,SAAS;AACnF,UAAM,IAAI,cAAc,kBAAkB,GAAG,MAAM,MAAM,qBAAqB,SAAS;AAAA,EACzF;AACF;AAEA,SAAS,gBAAgB,QAAwB;AAC/C,MAAI,CAAC,OAAO,MAAM,KAAK,GAAG;AACxB,UAAM,IAAI,cAAc,kBAAkB,4BAA4B,eAAe;AAAA,EACvF;AAEA,MAAI,CAAC,OAAO,MAAM,KAAK,GAAG;AACxB,UAAM,IAAI,cAAc,kBAAkB,4BAA4B,eAAe;AAAA,EACvF;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,OAAO,OAAO,MAAM,KAAK;AAAA,IACzB,OAAO,OAAO,MAAM,KAAK;AAAA,IACzB,WAAW,OAAO,aAAa,eAAe,OAAO,SAAS,OAAO,MAAM;AAAA,EAC7E;AACF;AAEA,SAAS,eAAe,SAAqC;AAC3D,MAAI,CAAC,QAAQ,UAAU,MAAM,KAAK,KAAK,CAAC,QAAQ,UAAU,IAAI,KAAK,GAAG;AACpE,UAAM,IAAI,cAAc,oBAAoB,8BAA8B,UAAU;AAAA,EACtF;AAEA,MAAI,QAAQ,UAAU,WAAc,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,MAAM;AAC7E,UAAM,IAAI,cAAc,iBAAiB,mCAAmC,OAAO;AAAA,EACrF;AAEA,MAAI,QAAQ,WAAW,UAAa,QAAQ,SAAS,GAAG;AACtD,UAAM,IAAI,cAAc,kBAAkB,+BAA+B,QAAQ;AAAA,EACnF;AAEA,MAAI,QAAQ,QAAQ,QAAQ,MAAM,QAAQ,OAAO,QAAQ,IAAI;AAC3D,UAAM,IAAI,cAAc,sBAAsB,0BAA0B,MAAM;AAAA,EAChF;AAEA,SAAO,aAAa;AAAA,IAClB,GAAG;AAAA,IACH,UAAU,aAAa;AAAA,MACrB,MAAM,QAAQ,SAAS,KAAK,KAAK;AAAA,MACjC,IAAI,QAAQ,SAAS,GAAG,KAAK;AAAA,MAC7B,OAAO,QAAQ,SAAS,OAAO,KAAK;AAAA,IACtC,CAAC;AAAA,IACD,QAAQ,QAAQ,QAAQ,KAAK,KAAK;AAAA,IAClC,OAAO,QAAQ,OAAO,KAAK,KAAK;AAAA,IAChC,SAAS,QAAQ,SAAS,KAAK,KAAK;AAAA,IACpC,SAAS,QAAQ,UACb,OAAO;AAAA,MACL,MAAM;AAAA,QACJ,IAAI,IAAI,QAAQ,QAAQ,IAAI,CAAC,WAAW,OAAO,KAAK,EAAE,YAAY,CAAW,CAAC;AAAA,MAChF;AAAA,IACF,IACA;AAAA,IACJ,OAAO,QAAQ,SAAS;AAAA,IACxB,QAAQ,QAAQ,UAAU;AAAA,EAC5B,CAAC;AACH;AAEA,SAAS,eAAe,OAAuB,SAA6B;AAC1E,SAAO,CAAC,SAAS,UAAU,QAAQ,SAAS,MAAM,MAAM;AAC1D;AAEA,SAAS,aAAa,OAAuB,SAAkB;AAC7D,SAAO,CAAC,WAAW,MAAM,MAAM,OAAO;AACxC;AAEA,SAAS,aAAa,OAAuB,MAAa,IAAW;AACnE,UAAQ,CAAC,QAAQ,MAAM,aAAa,UAAU,CAAC,MAAM,MAAM,aAAa;AAC1E;AAEA,SAAS,cAAc,OAAuB,QAAiB;AAC7D,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,SAAO,eAAe,KAAK,EAAE,YAAY,EAAE,SAAS,OAAO,YAAY,CAAC;AAC1E;AAEA,SAAS,eAAe,OAAuB;AAC7C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,SAAS;AAAA,IACf,MAAM,MAAM;AAAA,IACZ,MAAM,UAAU,KAAK,UAAU,MAAM,OAAO,IAAI;AAAA,IAChD,IAAI,MAAM,WAAW,CAAC,GAAG,QAAQ,CAAC,WAAW;AAAA,MAC3C,OAAO;AAAA,MACP,OAAO;AAAA,MACP,eAAe,OAAO,MAAM;AAAA,MAC5B,eAAe,OAAO,KAAK;AAAA,IAC7B,CAAC;AAAA,EACH,EAAE,KAAK,GAAG;AACZ;AAEA,SAAS,eAAe,GAAmB,GAAmB;AAC5D,QAAM,OAAO,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ;AACzD,SAAO,QAAQ,EAAE,GAAG,cAAc,EAAE,EAAE;AACxC;AAEA,SAAS,eAAe,OAA2B;AACjD,MAAI,iBAAiB,MAAM;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,OAAO;AACzB,MAAI,cAAc,YAAY,cAAc,YAAY,cAAc,WAAW;AAC/E,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEA,SAAS,aAAa,QAAwC;AAC5D,SAAO,aAAa;AAAA,IAClB,GAAG;AAAA,IACH,UAAU,aAAa,OAAO,QAAQ;AAAA,IACtC,OAAO,aAAa,OAAO,KAAK;AAAA,IAChC,SAAS,OAAO,SAAS,IAAI,CAAC,WAAW,aAAa,MAAM,CAAC;AAAA,IAC7D,SAAS,OAAO,UAAU,aAAa,OAAO,OAAO,IAAI;AAAA,IACzD,UAAU,OAAO,WAAW,aAAa,OAAO,QAAQ,IAAI;AAAA,EAC9D,CAAC;AACH;AAEA,SAAS,aAA+B,OAAuB;AAC7D,SAAO,OAAO,OAAO,KAAK;AAC5B;AAEA,SAAS,eAAe,OAAgB;AACtC,MAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,MAAM;AACzB,WAAO,MAAM,YAAY;AAAA,EAC3B;AAEA,SAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;AAEA,SAAS,WAAW;AAClB,MAAI,WAAW,QAAQ,YAAY;AACjC,WAAO,WAAW,OAAO,WAAW;AAAA,EACtC;AAEA,SAAO,OAAO,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC;AAC9E;AAEA,SAAS,KAAK,IAAY;AACxB,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,WAAW,SAAS,EAAE,CAAC;AACpE;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|