@beezping/adapter-memory 0.6.0 → 0.7.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/dist/index.js CHANGED
@@ -9,28 +9,21 @@ function clampPagination(query) {
9
9
  const limit = Math.min(toPositiveInteger(query.limit, DEFAULT_PAGE_LIMIT), MAX_PAGE_LIMIT);
10
10
  return { page, limit, skip: (page - 1) * limit };
11
11
  }
12
- function isUnreachableOffset(skip) {
13
- return !Number.isSafeInteger(skip);
12
+ function sortTime(record) {
13
+ const time = record.createdAt.getTime();
14
+ return Number.isNaN(time) ? Number.MIN_SAFE_INTEGER : time;
15
+ }
16
+ function matchesFeedbackQuery(record, query) {
17
+ const { type, status, statuses, url, urlPattern, search } = query;
18
+ return record.projectName === query.projectName && (!type || record.type === type) && // `statuses` (bucket / any-of) wins over the exact `status` filter when
19
+ // both are present; an empty array is treated as absent.
20
+ (statuses && statuses.length > 0 ? statuses.includes(record.status) : !status || record.status === status) && (!url || record.url === url) && (!urlPattern || record.urlPattern === urlPattern) && (!search || record.message.toLowerCase().includes(search.toLowerCase()));
14
21
  }
15
22
  function applyFeedbackFilters(items, query) {
16
- let results = items.filter((f) => f.projectName === query.projectName);
17
- if (query.type) results = results.filter((f) => f.type === query.type);
18
- if (query.statuses && query.statuses.length > 0) {
19
- const allowed = query.statuses;
20
- results = results.filter((f) => allowed.includes(f.status));
21
- } else if (query.status) {
22
- results = results.filter((f) => f.status === query.status);
23
- }
24
- if (query.url) results = results.filter((f) => f.url === query.url);
25
- if (query.urlPattern) results = results.filter((f) => f.urlPattern === query.urlPattern);
26
- if (query.search) {
27
- const s = query.search.toLowerCase();
28
- results = results.filter((f) => f.message.toLowerCase().includes(s));
29
- }
30
- results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
23
+ const results = items.filter((f) => matchesFeedbackQuery(f, query));
24
+ results.sort((a, b) => sortTime(b) - sortTime(a));
31
25
  const total = results.length;
32
26
  const { limit, skip } = clampPagination(query);
33
- if (isUnreachableOffset(skip)) return { feedbacks: [], total };
34
27
  return { feedbacks: results.slice(skip, skip + limit), total };
35
28
  }
36
29
 
@@ -43,6 +36,7 @@ function hasOwn(value, key) {
43
36
  }
44
37
 
45
38
  // ../core/src/types.ts
39
+ var MAX_COMMENTS_PER_FEEDBACK = 100;
46
40
  var StoreNotFoundError = class extends Error {
47
41
  code = "STORE_NOT_FOUND";
48
42
  constructor(message = "Record not found", options) {
@@ -64,6 +58,13 @@ var StorePersistenceError = class extends Error {
64
58
  this.name = "StorePersistenceError";
65
59
  }
66
60
  };
61
+ var StoreLimitError = class extends Error {
62
+ code = "STORE_LIMIT";
63
+ constructor(message = "Store limit reached", options) {
64
+ super(message, options);
65
+ this.name = "StoreLimitError";
66
+ }
67
+ };
67
68
  function hasErrorCode(error, code) {
68
69
  return hasOwn(error, "code") && error.code === code;
69
70
  }
@@ -99,6 +100,18 @@ function buildAnnotationRecord(input, ctx) {
99
100
  createdAt: ctx.now
100
101
  };
101
102
  }
103
+ function buildCommentRecord(input, ctx) {
104
+ return {
105
+ id: ctx.id,
106
+ feedbackId: ctx.feedbackId,
107
+ body: input.body,
108
+ authorName: input.authorName,
109
+ authorEmail: input.authorEmail,
110
+ authorRole: input.authorRole,
111
+ clientId: input.clientId,
112
+ createdAt: ctx.now ?? /* @__PURE__ */ new Date()
113
+ };
114
+ }
102
115
  function buildFeedbackRecord(input, ctx) {
103
116
  const now = ctx.now ?? /* @__PURE__ */ new Date();
104
117
  return {
@@ -126,23 +139,24 @@ function buildFeedbackRecord(input, ctx) {
126
139
  };
127
140
  }
128
141
  function createCollectionStore(backend) {
129
- let mutationQueueTail = Promise.resolve();
130
- const runSerializedMutation = (mutation) => {
131
- const mutationResult = mutationQueueTail.then(mutation);
132
- mutationQueueTail = mutationResult.then(
133
- () => void 0,
134
- () => void 0
135
- );
136
- return mutationResult;
137
- };
138
- const insertFeedbackIfAbsent = async (data) => {
139
- const feedbacks = await backend.load();
142
+ let tail = Promise.resolve();
143
+ function mutate(mutation) {
144
+ const result = tail.then(() => {
145
+ const loaded = backend.load();
146
+ return Array.isArray(loaded) ? mutation(loaded) : loaded.then(mutation);
147
+ });
148
+ tail = result.catch(() => {
149
+ });
150
+ return result;
151
+ }
152
+ const createFeedbackIfAbsent = (data) => mutate(async (feedbacks) => {
140
153
  const existing = feedbacks.find((f) => f.clientId === data.clientId);
141
154
  if (existing) return { feedback: existing, created: false };
142
155
  const record = buildFeedbackRecord(data, {
143
156
  id: backend.generateId(),
144
157
  annotationId: () => backend.generateId()
145
158
  });
159
+ if (backend.comments) record.comments = [];
146
160
  const next = [record, ...feedbacks];
147
161
  try {
148
162
  await backend.persist(next);
@@ -152,9 +166,8 @@ function createCollectionStore(backend) {
152
166
  await backend.persist(next);
153
167
  }
154
168
  return { feedback: record, created: true };
155
- };
156
- const createFeedbackIfAbsent = (data) => runSerializedMutation(() => insertFeedbackIfAbsent(data));
157
- return {
169
+ });
170
+ const store = {
158
171
  createFeedbackIfAbsent,
159
172
  async createFeedback(data) {
160
173
  return (await createFeedbackIfAbsent(data)).feedback;
@@ -165,39 +178,55 @@ function createCollectionStore(backend) {
165
178
  async findByClientId(clientId) {
166
179
  return (await backend.load()).find((f) => f.clientId === clientId) ?? null;
167
180
  },
168
- updateFeedback(id, data) {
169
- return runSerializedMutation(async () => {
170
- const feedbacks = await backend.load();
171
- const current = feedbacks.find((f) => f.id === id);
172
- if (!current) throw new StoreNotFoundError();
173
- const updated = {
174
- ...current,
175
- status: data.status,
176
- resolvedAt: data.resolvedAt,
177
- updatedAt: /* @__PURE__ */ new Date()
178
- };
179
- await backend.persist(feedbacks.map((f) => f === current ? updated : f));
180
- return updated;
181
- });
182
- },
183
- deleteFeedback(id) {
184
- return runSerializedMutation(async () => {
185
- const feedbacks = await backend.load();
186
- if (!feedbacks.some((f) => f.id === id)) throw new StoreNotFoundError();
187
- await backend.persist(feedbacks.filter((f) => f.id !== id));
188
- });
189
- },
190
- deleteAllFeedbacks(projectName) {
191
- return runSerializedMutation(async () => {
192
- const feedbacks = await backend.load();
193
- await backend.persist(feedbacks.filter((f) => f.projectName !== projectName));
194
- });
195
- },
181
+ updateFeedback: (id, data) => mutate(async (feedbacks) => {
182
+ const current = feedbacks.find((f) => f.id === id);
183
+ if (!current) throw new StoreNotFoundError();
184
+ const updated = {
185
+ ...current,
186
+ status: data.status,
187
+ resolvedAt: data.resolvedAt,
188
+ updatedAt: /* @__PURE__ */ new Date()
189
+ };
190
+ await backend.persist(feedbacks.map((f) => f === current ? updated : f));
191
+ return updated;
192
+ }),
193
+ deleteFeedback: (id) => mutate(async (feedbacks) => {
194
+ if (!feedbacks.some((f) => f.id === id)) throw new StoreNotFoundError();
195
+ await backend.persist(feedbacks.filter((f) => f.id !== id));
196
+ }),
197
+ deleteAllFeedbacks: (projectName) => mutate(async (feedbacks) => {
198
+ await backend.persist(feedbacks.filter((f) => f.projectName !== projectName));
199
+ }),
196
200
  async verifyProjectOwnership(id, projectName) {
197
201
  const fb = (await backend.load()).find((f) => f.id === id);
198
202
  return fb !== void 0 && fb.projectName === projectName;
199
203
  }
200
204
  };
205
+ if (!backend.comments) return store;
206
+ return {
207
+ ...store,
208
+ addComment: (feedbackId, data) => mutate(async (feedbacks) => {
209
+ const existing = feedbacks.flatMap((f) => f.comments ?? []).find((c) => c.clientId === data.clientId);
210
+ if (existing) return existing;
211
+ const current = feedbacks.find((f) => f.id === feedbackId);
212
+ if (!current) throw new StoreNotFoundError();
213
+ const thread = current.comments ?? [];
214
+ if (data.authorRole === "client" && thread.filter((c) => c.authorRole === "client").length >= MAX_COMMENTS_PER_FEEDBACK) {
215
+ throw new StoreLimitError(`A thread holds at most ${MAX_COMMENTS_PER_FEEDBACK} client comments`);
216
+ }
217
+ const comment = buildCommentRecord(data, { id: backend.generateId(), feedbackId });
218
+ const updated = { ...current, comments: [...thread, comment] };
219
+ await backend.persist(feedbacks.map((f) => f === current ? updated : f));
220
+ return comment;
221
+ }),
222
+ deleteComment: (feedbackId, commentId) => mutate(async (feedbacks) => {
223
+ const current = feedbacks.find((f) => f.id === feedbackId);
224
+ const thread = current?.comments ?? [];
225
+ if (!current || !thread.some((c) => c.id === commentId)) throw new StoreNotFoundError();
226
+ const updated = { ...current, comments: thread.filter((c) => c.id !== commentId) };
227
+ await backend.persist(feedbacks.map((f) => f === current ? updated : f));
228
+ })
229
+ };
201
230
  }
202
231
 
203
232
  // src/index.ts
@@ -209,7 +238,9 @@ var MemoryStore = class {
209
238
  persist: (next) => {
210
239
  this.feedbacks = next;
211
240
  },
212
- generateId: () => `mem-${this.idCounter++}-${Date.now().toString(36)}`
241
+ generateId: () => `mem-${this.idCounter++}-${Date.now().toString(36)}`,
242
+ // Records live in memory: comment dates stay Dates.
243
+ comments: true
213
244
  });
214
245
  createFeedback(data) {
215
246
  return this.engine.createFeedback(data);
@@ -235,6 +266,12 @@ var MemoryStore = class {
235
266
  verifyProjectOwnership(id, projectName) {
236
267
  return this.engine.verifyProjectOwnership(id, projectName);
237
268
  }
269
+ addComment(feedbackId, data) {
270
+ return this.engine.addComment(feedbackId, data);
271
+ }
272
+ deleteComment(feedbackId, commentId) {
273
+ return this.engine.deleteComment(feedbackId, commentId);
274
+ }
238
275
  /** Remove all data from this store instance. */
239
276
  clear() {
240
277
  this.feedbacks = [];
@@ -244,6 +281,7 @@ var MemoryStore = class {
244
281
  export {
245
282
  MemoryStore,
246
283
  StoreDuplicateError,
284
+ StoreLimitError,
247
285
  StoreNotFoundError,
248
286
  StorePersistenceError,
249
287
  isStorePersistence