@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/LICENSE +21 -0
- package/README.md +10 -8
- package/dist/beezping-core.d.cts +20 -0
- package/dist/beezping-core.d.ts +20 -0
- package/dist/concurrency.d.cts +14 -0
- package/dist/concurrency.d.ts +14 -0
- package/dist/constants/schema.d.cts +239 -0
- package/dist/constants/schema.d.ts +239 -0
- package/dist/deep-link.d.cts +14 -0
- package/dist/deep-link.d.ts +14 -0
- package/dist/email.d.cts +8 -5
- package/dist/email.d.ts +8 -5
- package/dist/errors.d.cts +28 -14
- package/dist/errors.d.ts +28 -14
- package/dist/filters.d.cts +15 -7
- package/dist/filters.d.ts +15 -7
- package/dist/i18n.d.cts +18 -0
- package/dist/i18n.d.ts +18 -0
- package/dist/index.cjs +99 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.js +99 -61
- package/dist/index.js.map +1 -1
- package/dist/schema.d.cts +12 -195
- package/dist/schema.d.ts +12 -195
- package/dist/screenshot-storage.d.cts +55 -30
- package/dist/screenshot-storage.d.ts +55 -30
- package/dist/store-helpers.d.cts +62 -34
- package/dist/store-helpers.d.ts +62 -34
- package/dist/type-utils.d.cts +8 -0
- package/dist/type-utils.d.ts +8 -0
- package/dist/types.d.cts +413 -154
- package/dist/types.d.ts +413 -154
- package/dist/wire.d.cts +42 -9
- package/dist/wire.d.ts +42 -9
- package/package.json +7 -8
- package/dist/siteping-core.d.cts +0 -17
- package/dist/siteping-core.d.ts +0 -17
package/dist/email.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The one email pattern every
|
|
2
|
+
* The one email pattern every Beezping surface validates against.
|
|
3
3
|
*
|
|
4
4
|
* The widget's identity modal and the HTTP handler's schema used to disagree
|
|
5
5
|
* (a permissive regex on one side, an ASCII-only default on the other), so an
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
* sides, makes that drift impossible.
|
|
9
9
|
*
|
|
10
10
|
* Deliberately Unicode-aware: internationalised local parts and domains
|
|
11
|
-
* (`françois@exemple.fr`, `user@münchen.de`) are real
|
|
12
|
-
* the audience this widget serves.
|
|
13
|
-
*
|
|
11
|
+
* (`françois@exemple.fr`, `user@münchen.de`, `user@हिंदी.भारत`) are real
|
|
12
|
+
* addresses and common in the audience this widget serves. Combining marks
|
|
13
|
+
* (`\p{M}`) are allowed alongside letters — scripts like Devanagari need them,
|
|
14
|
+
* and NFD-composed input spells `ç` as `c` + U+0327 — except as the first
|
|
15
|
+
* character of a domain label. Structure follows the usual rules — a local
|
|
16
|
+
* part of 1 to 64 characters with no leading, trailing or doubled dot,
|
|
14
17
|
* dot-separated domain labels of at most 63 characters that neither start nor
|
|
15
18
|
* end with a hyphen, and a final label of at least two characters.
|
|
16
19
|
*/
|
|
17
20
|
export declare const EMAIL_PATTERN: RegExp;
|
|
18
|
-
/** Whether `value` is an email address
|
|
21
|
+
/** Whether `value` is an email address Beezping accepts — see {@link EMAIL_PATTERN}. */
|
|
19
22
|
export declare function isValidEmail(value: string): boolean;
|
package/dist/email.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The one email pattern every
|
|
2
|
+
* The one email pattern every Beezping surface validates against.
|
|
3
3
|
*
|
|
4
4
|
* The widget's identity modal and the HTTP handler's schema used to disagree
|
|
5
5
|
* (a permissive regex on one side, an ASCII-only default on the other), so an
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
* sides, makes that drift impossible.
|
|
9
9
|
*
|
|
10
10
|
* Deliberately Unicode-aware: internationalised local parts and domains
|
|
11
|
-
* (`françois@exemple.fr`, `user@münchen.de`) are real
|
|
12
|
-
* the audience this widget serves.
|
|
13
|
-
*
|
|
11
|
+
* (`françois@exemple.fr`, `user@münchen.de`, `user@हिंदी.भारत`) are real
|
|
12
|
+
* addresses and common in the audience this widget serves. Combining marks
|
|
13
|
+
* (`\p{M}`) are allowed alongside letters — scripts like Devanagari need them,
|
|
14
|
+
* and NFD-composed input spells `ç` as `c` + U+0327 — except as the first
|
|
15
|
+
* character of a domain label. Structure follows the usual rules — a local
|
|
16
|
+
* part of 1 to 64 characters with no leading, trailing or doubled dot,
|
|
14
17
|
* dot-separated domain labels of at most 63 characters that neither start nor
|
|
15
18
|
* end with a hyphen, and a final label of at least two characters.
|
|
16
19
|
*/
|
|
17
20
|
export declare const EMAIL_PATTERN: RegExp;
|
|
18
|
-
/** Whether `value` is an email address
|
|
21
|
+
/** Whether `value` is an email address Beezping accepts — see {@link EMAIL_PATTERN}. */
|
|
19
22
|
export declare function isValidEmail(value: string): boolean;
|
package/dist/errors.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typed error hierarchy for
|
|
2
|
+
* Typed error hierarchy for Beezping client/server boundaries.
|
|
3
3
|
*
|
|
4
4
|
* Consumers can `instanceof`-check or read `code` / `retryable` instead of
|
|
5
5
|
* pattern-matching error messages. Designed to be additive on top of the
|
|
@@ -7,35 +7,49 @@
|
|
|
7
7
|
* remain the canonical signals for server-side store implementations.
|
|
8
8
|
*
|
|
9
9
|
* Usage on the widget side (api-client.ts):
|
|
10
|
-
* - fetch failures / aborts / timeouts → `
|
|
11
|
-
* - HTTP 4xx (except 401/403) → `
|
|
12
|
-
* - HTTP 401 / 403 → `
|
|
13
|
-
* - everything else → `
|
|
10
|
+
* - fetch failures / aborts / timeouts → `BeezpingNetworkError` (retryable)
|
|
11
|
+
* - HTTP 4xx (except 401/403) → `BeezpingValidationError` (not retryable)
|
|
12
|
+
* - HTTP 401 / 403 → `BeezpingAuthError` (not retryable)
|
|
13
|
+
* - everything else → `BeezpingError` generic
|
|
14
|
+
*
|
|
15
|
+
* Store mode (store-client.ts): a `createFeedback` that does not settle in
|
|
16
|
+
* time → `BeezpingError` with code `"TIMEOUT"` (retryable).
|
|
14
17
|
*
|
|
15
18
|
* `retryable` is meta information surfaced to host apps that want to wire
|
|
16
19
|
* their own retry/queue/backoff strategy — the widget already retries
|
|
17
20
|
* network failures via its built-in retry queue.
|
|
18
21
|
*/
|
|
19
22
|
/**
|
|
20
|
-
* Discriminant string carried by every `
|
|
23
|
+
* Discriminant string carried by every `BeezpingError`. Subclasses pin a
|
|
21
24
|
* literal value; the base class accepts a wider string so userland can
|
|
22
25
|
* extend the hierarchy without colliding with built-ins.
|
|
23
26
|
*/
|
|
24
|
-
export type
|
|
25
|
-
export declare class
|
|
27
|
+
export type BeezpingErrorCode = "NETWORK" | "VALIDATION" | "AUTH" | "SERVER" | "TIMEOUT" | (string & {});
|
|
28
|
+
export declare class BeezpingError<TCode extends BeezpingErrorCode = BeezpingErrorCode> extends Error {
|
|
26
29
|
readonly code: TCode;
|
|
27
30
|
readonly retryable: boolean;
|
|
28
31
|
constructor(message: string, code: TCode, retryable: boolean);
|
|
29
32
|
}
|
|
30
33
|
/** Network-level failure: connection refused, DNS, CORS, timeout, abort. Retryable. */
|
|
31
|
-
export declare class
|
|
34
|
+
export declare class BeezpingNetworkError extends BeezpingError<"NETWORK"> {
|
|
32
35
|
constructor(message: string);
|
|
33
36
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Server rejected the request (4xx, not auth). Validation problem on the
|
|
39
|
+
* client side. `status` is the HTTP status when a response said so: a 404
|
|
40
|
+
* (the target is gone) and a 409 (a full thread) call for other answers
|
|
41
|
+
* than a 400.
|
|
42
|
+
*/
|
|
43
|
+
export declare class BeezpingValidationError extends BeezpingError<"VALIDATION"> {
|
|
44
|
+
readonly status: number | undefined;
|
|
45
|
+
constructor(message: string, status?: number);
|
|
37
46
|
}
|
|
38
47
|
/** Server rejected auth (401 or 403). Not retryable without fresh credentials. */
|
|
39
|
-
export declare class
|
|
40
|
-
|
|
48
|
+
export declare class BeezpingAuthError extends BeezpingError<"AUTH"> {
|
|
49
|
+
/**
|
|
50
|
+
* `401`: the credentials are missing or no longer work — drop a dead token
|
|
51
|
+
* on this one. `403`: they work, and the server's policy refuses this request.
|
|
52
|
+
*/
|
|
53
|
+
readonly status: 401 | 403;
|
|
54
|
+
constructor(message: string, status: 401 | 403);
|
|
41
55
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typed error hierarchy for
|
|
2
|
+
* Typed error hierarchy for Beezping client/server boundaries.
|
|
3
3
|
*
|
|
4
4
|
* Consumers can `instanceof`-check or read `code` / `retryable` instead of
|
|
5
5
|
* pattern-matching error messages. Designed to be additive on top of the
|
|
@@ -7,35 +7,49 @@
|
|
|
7
7
|
* remain the canonical signals for server-side store implementations.
|
|
8
8
|
*
|
|
9
9
|
* Usage on the widget side (api-client.ts):
|
|
10
|
-
* - fetch failures / aborts / timeouts → `
|
|
11
|
-
* - HTTP 4xx (except 401/403) → `
|
|
12
|
-
* - HTTP 401 / 403 → `
|
|
13
|
-
* - everything else → `
|
|
10
|
+
* - fetch failures / aborts / timeouts → `BeezpingNetworkError` (retryable)
|
|
11
|
+
* - HTTP 4xx (except 401/403) → `BeezpingValidationError` (not retryable)
|
|
12
|
+
* - HTTP 401 / 403 → `BeezpingAuthError` (not retryable)
|
|
13
|
+
* - everything else → `BeezpingError` generic
|
|
14
|
+
*
|
|
15
|
+
* Store mode (store-client.ts): a `createFeedback` that does not settle in
|
|
16
|
+
* time → `BeezpingError` with code `"TIMEOUT"` (retryable).
|
|
14
17
|
*
|
|
15
18
|
* `retryable` is meta information surfaced to host apps that want to wire
|
|
16
19
|
* their own retry/queue/backoff strategy — the widget already retries
|
|
17
20
|
* network failures via its built-in retry queue.
|
|
18
21
|
*/
|
|
19
22
|
/**
|
|
20
|
-
* Discriminant string carried by every `
|
|
23
|
+
* Discriminant string carried by every `BeezpingError`. Subclasses pin a
|
|
21
24
|
* literal value; the base class accepts a wider string so userland can
|
|
22
25
|
* extend the hierarchy without colliding with built-ins.
|
|
23
26
|
*/
|
|
24
|
-
export type
|
|
25
|
-
export declare class
|
|
27
|
+
export type BeezpingErrorCode = "NETWORK" | "VALIDATION" | "AUTH" | "SERVER" | "TIMEOUT" | (string & {});
|
|
28
|
+
export declare class BeezpingError<TCode extends BeezpingErrorCode = BeezpingErrorCode> extends Error {
|
|
26
29
|
readonly code: TCode;
|
|
27
30
|
readonly retryable: boolean;
|
|
28
31
|
constructor(message: string, code: TCode, retryable: boolean);
|
|
29
32
|
}
|
|
30
33
|
/** Network-level failure: connection refused, DNS, CORS, timeout, abort. Retryable. */
|
|
31
|
-
export declare class
|
|
34
|
+
export declare class BeezpingNetworkError extends BeezpingError<"NETWORK"> {
|
|
32
35
|
constructor(message: string);
|
|
33
36
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Server rejected the request (4xx, not auth). Validation problem on the
|
|
39
|
+
* client side. `status` is the HTTP status when a response said so: a 404
|
|
40
|
+
* (the target is gone) and a 409 (a full thread) call for other answers
|
|
41
|
+
* than a 400.
|
|
42
|
+
*/
|
|
43
|
+
export declare class BeezpingValidationError extends BeezpingError<"VALIDATION"> {
|
|
44
|
+
readonly status: number | undefined;
|
|
45
|
+
constructor(message: string, status?: number);
|
|
37
46
|
}
|
|
38
47
|
/** Server rejected auth (401 or 403). Not retryable without fresh credentials. */
|
|
39
|
-
export declare class
|
|
40
|
-
|
|
48
|
+
export declare class BeezpingAuthError extends BeezpingError<"AUTH"> {
|
|
49
|
+
/**
|
|
50
|
+
* `401`: the credentials are missing or no longer work — drop a dead token
|
|
51
|
+
* on this one. `403`: they work, and the server's policy refuses this request.
|
|
52
|
+
*/
|
|
53
|
+
readonly status: 401 | 403;
|
|
54
|
+
constructor(message: string, status: 401 | 403);
|
|
41
55
|
}
|
package/dist/filters.d.cts
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* near-identical copies of the same logic. Any adapter that holds an
|
|
5
5
|
* in-memory snapshot of feedbacks can use it.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* A record matches when it passes every active filter (see
|
|
8
|
+
* {@link matchesFeedbackQuery}):
|
|
9
|
+
* - projectName (always required)
|
|
10
|
+
* - type
|
|
11
|
+
* - status / statuses (`statuses` bucket wins when both are set)
|
|
12
|
+
* - url
|
|
13
|
+
* - urlPattern
|
|
14
|
+
* - search (lowercase substring match on `message`)
|
|
14
15
|
*
|
|
15
16
|
* Pagination goes through `clampPagination`: `limit` capped at 100, `page`
|
|
16
17
|
* 1-based, both clamped up to 1 rather than indexing backwards from the end
|
|
@@ -63,6 +64,13 @@ export declare function clampPagination(query: Pick<FeedbackQuery, "page" | "lim
|
|
|
63
64
|
* @returns `true` when no row can sit at that offset.
|
|
64
65
|
*/
|
|
65
66
|
export declare function isUnreachableOffset(skip: number): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether one record passes every filter of `query` — the filter half of
|
|
69
|
+
* {@link applyFeedbackFilters}, exposed so a client holding a single record
|
|
70
|
+
* (e.g. the dashboard deciding whether an optimistic edit still belongs in
|
|
71
|
+
* its list) applies exactly the stores' semantics. Pagination is ignored.
|
|
72
|
+
*/
|
|
73
|
+
export declare function matchesFeedbackQuery(record: FeedbackRecord, query: FeedbackQuery): boolean;
|
|
66
74
|
/**
|
|
67
75
|
* Apply the standard feedback filter + pagination pipeline against an
|
|
68
76
|
* in-memory snapshot. Used by `MemoryStore.getFeedbacks` and
|
package/dist/filters.d.ts
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* near-identical copies of the same logic. Any adapter that holds an
|
|
5
5
|
* in-memory snapshot of feedbacks can use it.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* A record matches when it passes every active filter (see
|
|
8
|
+
* {@link matchesFeedbackQuery}):
|
|
9
|
+
* - projectName (always required)
|
|
10
|
+
* - type
|
|
11
|
+
* - status / statuses (`statuses` bucket wins when both are set)
|
|
12
|
+
* - url
|
|
13
|
+
* - urlPattern
|
|
14
|
+
* - search (lowercase substring match on `message`)
|
|
14
15
|
*
|
|
15
16
|
* Pagination goes through `clampPagination`: `limit` capped at 100, `page`
|
|
16
17
|
* 1-based, both clamped up to 1 rather than indexing backwards from the end
|
|
@@ -63,6 +64,13 @@ export declare function clampPagination(query: Pick<FeedbackQuery, "page" | "lim
|
|
|
63
64
|
* @returns `true` when no row can sit at that offset.
|
|
64
65
|
*/
|
|
65
66
|
export declare function isUnreachableOffset(skip: number): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether one record passes every filter of `query` — the filter half of
|
|
69
|
+
* {@link applyFeedbackFilters}, exposed so a client holding a single record
|
|
70
|
+
* (e.g. the dashboard deciding whether an optimistic edit still belongs in
|
|
71
|
+
* its list) applies exactly the stores' semantics. Pagination is ignored.
|
|
72
|
+
*/
|
|
73
|
+
export declare function matchesFeedbackQuery(record: FeedbackRecord, query: FeedbackQuery): boolean;
|
|
66
74
|
/**
|
|
67
75
|
* Apply the standard feedback filter + pagination pipeline against an
|
|
68
76
|
* in-memory snapshot. Used by `MemoryStore.getFeedbacks` and
|
package/dist/i18n.d.cts
CHANGED
|
@@ -48,6 +48,24 @@ export interface I18n<T> {
|
|
|
48
48
|
*/
|
|
49
49
|
registerLocale(code: string, translations: Partial<T>): void;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* `locale` as `Intl` accepts it. Locale options take any string, and a
|
|
53
|
+
* backend-style tag (`fr_FR` from PHP / WordPress) makes every `Intl`
|
|
54
|
+
* constructor and `toLocale*` call throw a RangeError: `_` reads as `-`, and
|
|
55
|
+
* a tag `Intl` still rejects falls back to English, as `createT` does.
|
|
56
|
+
* Silent, so it is safe on a render path; see `canonicalizeLocale` for the
|
|
57
|
+
* one-time, warning variant.
|
|
58
|
+
*/
|
|
59
|
+
export declare function intlLocale(locale: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* The host's locale tag as `Intl` accepts it. `BeezpingLocale` takes any
|
|
62
|
+
* string, and PHP / WordPress hand out POSIX-style tags (`fr_FR`, `pt_BR`)
|
|
63
|
+
* that every `Intl` constructor rejects with a RangeError: `_` becomes `-`,
|
|
64
|
+
* then the tag is canonicalised (`fr-FR`). Falls back to `"en"`, with a
|
|
65
|
+
* warning, only when the tag is still invalid (empty, garbage) — meant to run
|
|
66
|
+
* once, where the config is read.
|
|
67
|
+
*/
|
|
68
|
+
export declare function canonicalizeLocale(locale: string): string;
|
|
51
69
|
/**
|
|
52
70
|
* Build the i18n machinery for one message catalog.
|
|
53
71
|
*
|
package/dist/i18n.d.ts
CHANGED
|
@@ -48,6 +48,24 @@ export interface I18n<T> {
|
|
|
48
48
|
*/
|
|
49
49
|
registerLocale(code: string, translations: Partial<T>): void;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* `locale` as `Intl` accepts it. Locale options take any string, and a
|
|
53
|
+
* backend-style tag (`fr_FR` from PHP / WordPress) makes every `Intl`
|
|
54
|
+
* constructor and `toLocale*` call throw a RangeError: `_` reads as `-`, and
|
|
55
|
+
* a tag `Intl` still rejects falls back to English, as `createT` does.
|
|
56
|
+
* Silent, so it is safe on a render path; see `canonicalizeLocale` for the
|
|
57
|
+
* one-time, warning variant.
|
|
58
|
+
*/
|
|
59
|
+
export declare function intlLocale(locale: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* The host's locale tag as `Intl` accepts it. `BeezpingLocale` takes any
|
|
62
|
+
* string, and PHP / WordPress hand out POSIX-style tags (`fr_FR`, `pt_BR`)
|
|
63
|
+
* that every `Intl` constructor rejects with a RangeError: `_` becomes `-`,
|
|
64
|
+
* then the tag is canonicalised (`fr-FR`). Falls back to `"en"`, with a
|
|
65
|
+
* warning, only when the tag is still invalid (empty, garbage) — meant to run
|
|
66
|
+
* once, where the config is read.
|
|
67
|
+
*/
|
|
68
|
+
export declare function canonicalizeLocale(locale: string): string;
|
|
51
69
|
/**
|
|
52
70
|
* Build the i18n machinery for one message catalog.
|
|
53
71
|
*
|
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
MemoryStore: () => MemoryStore,
|
|
24
24
|
StoreDuplicateError: () => StoreDuplicateError,
|
|
25
|
+
StoreLimitError: () => StoreLimitError,
|
|
25
26
|
StoreNotFoundError: () => StoreNotFoundError,
|
|
26
27
|
StorePersistenceError: () => StorePersistenceError,
|
|
27
28
|
isStorePersistence: () => isStorePersistence
|
|
@@ -39,28 +40,21 @@ function clampPagination(query) {
|
|
|
39
40
|
const limit = Math.min(toPositiveInteger(query.limit, DEFAULT_PAGE_LIMIT), MAX_PAGE_LIMIT);
|
|
40
41
|
return { page, limit, skip: (page - 1) * limit };
|
|
41
42
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
43
|
+
function sortTime(record) {
|
|
44
|
+
const time = record.createdAt.getTime();
|
|
45
|
+
return Number.isNaN(time) ? Number.MIN_SAFE_INTEGER : time;
|
|
46
|
+
}
|
|
47
|
+
function matchesFeedbackQuery(record, query) {
|
|
48
|
+
const { type, status, statuses, url, urlPattern, search } = query;
|
|
49
|
+
return record.projectName === query.projectName && (!type || record.type === type) && // `statuses` (bucket / any-of) wins over the exact `status` filter when
|
|
50
|
+
// both are present; an empty array is treated as absent.
|
|
51
|
+
(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()));
|
|
44
52
|
}
|
|
45
53
|
function applyFeedbackFilters(items, query) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (query.statuses && query.statuses.length > 0) {
|
|
49
|
-
const allowed = query.statuses;
|
|
50
|
-
results = results.filter((f) => allowed.includes(f.status));
|
|
51
|
-
} else if (query.status) {
|
|
52
|
-
results = results.filter((f) => f.status === query.status);
|
|
53
|
-
}
|
|
54
|
-
if (query.url) results = results.filter((f) => f.url === query.url);
|
|
55
|
-
if (query.urlPattern) results = results.filter((f) => f.urlPattern === query.urlPattern);
|
|
56
|
-
if (query.search) {
|
|
57
|
-
const s = query.search.toLowerCase();
|
|
58
|
-
results = results.filter((f) => f.message.toLowerCase().includes(s));
|
|
59
|
-
}
|
|
60
|
-
results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
54
|
+
const results = items.filter((f) => matchesFeedbackQuery(f, query));
|
|
55
|
+
results.sort((a, b) => sortTime(b) - sortTime(a));
|
|
61
56
|
const total = results.length;
|
|
62
57
|
const { limit, skip } = clampPagination(query);
|
|
63
|
-
if (isUnreachableOffset(skip)) return { feedbacks: [], total };
|
|
64
58
|
return { feedbacks: results.slice(skip, skip + limit), total };
|
|
65
59
|
}
|
|
66
60
|
|
|
@@ -73,6 +67,7 @@ function hasOwn(value, key) {
|
|
|
73
67
|
}
|
|
74
68
|
|
|
75
69
|
// ../core/src/types.ts
|
|
70
|
+
var MAX_COMMENTS_PER_FEEDBACK = 100;
|
|
76
71
|
var StoreNotFoundError = class extends Error {
|
|
77
72
|
code = "STORE_NOT_FOUND";
|
|
78
73
|
constructor(message = "Record not found", options) {
|
|
@@ -94,6 +89,13 @@ var StorePersistenceError = class extends Error {
|
|
|
94
89
|
this.name = "StorePersistenceError";
|
|
95
90
|
}
|
|
96
91
|
};
|
|
92
|
+
var StoreLimitError = class extends Error {
|
|
93
|
+
code = "STORE_LIMIT";
|
|
94
|
+
constructor(message = "Store limit reached", options) {
|
|
95
|
+
super(message, options);
|
|
96
|
+
this.name = "StoreLimitError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
97
99
|
function hasErrorCode(error, code) {
|
|
98
100
|
return hasOwn(error, "code") && error.code === code;
|
|
99
101
|
}
|
|
@@ -129,6 +131,18 @@ function buildAnnotationRecord(input, ctx) {
|
|
|
129
131
|
createdAt: ctx.now
|
|
130
132
|
};
|
|
131
133
|
}
|
|
134
|
+
function buildCommentRecord(input, ctx) {
|
|
135
|
+
return {
|
|
136
|
+
id: ctx.id,
|
|
137
|
+
feedbackId: ctx.feedbackId,
|
|
138
|
+
body: input.body,
|
|
139
|
+
authorName: input.authorName,
|
|
140
|
+
authorEmail: input.authorEmail,
|
|
141
|
+
authorRole: input.authorRole,
|
|
142
|
+
clientId: input.clientId,
|
|
143
|
+
createdAt: ctx.now ?? /* @__PURE__ */ new Date()
|
|
144
|
+
};
|
|
145
|
+
}
|
|
132
146
|
function buildFeedbackRecord(input, ctx) {
|
|
133
147
|
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
134
148
|
return {
|
|
@@ -156,23 +170,24 @@ function buildFeedbackRecord(input, ctx) {
|
|
|
156
170
|
};
|
|
157
171
|
}
|
|
158
172
|
function createCollectionStore(backend) {
|
|
159
|
-
let
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
()
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
let tail = Promise.resolve();
|
|
174
|
+
function mutate(mutation) {
|
|
175
|
+
const result = tail.then(() => {
|
|
176
|
+
const loaded = backend.load();
|
|
177
|
+
return Array.isArray(loaded) ? mutation(loaded) : loaded.then(mutation);
|
|
178
|
+
});
|
|
179
|
+
tail = result.catch(() => {
|
|
180
|
+
});
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
const createFeedbackIfAbsent = (data) => mutate(async (feedbacks) => {
|
|
170
184
|
const existing = feedbacks.find((f) => f.clientId === data.clientId);
|
|
171
185
|
if (existing) return { feedback: existing, created: false };
|
|
172
186
|
const record = buildFeedbackRecord(data, {
|
|
173
187
|
id: backend.generateId(),
|
|
174
188
|
annotationId: () => backend.generateId()
|
|
175
189
|
});
|
|
190
|
+
if (backend.comments) record.comments = [];
|
|
176
191
|
const next = [record, ...feedbacks];
|
|
177
192
|
try {
|
|
178
193
|
await backend.persist(next);
|
|
@@ -182,9 +197,8 @@ function createCollectionStore(backend) {
|
|
|
182
197
|
await backend.persist(next);
|
|
183
198
|
}
|
|
184
199
|
return { feedback: record, created: true };
|
|
185
|
-
};
|
|
186
|
-
const
|
|
187
|
-
return {
|
|
200
|
+
});
|
|
201
|
+
const store = {
|
|
188
202
|
createFeedbackIfAbsent,
|
|
189
203
|
async createFeedback(data) {
|
|
190
204
|
return (await createFeedbackIfAbsent(data)).feedback;
|
|
@@ -195,39 +209,55 @@ function createCollectionStore(backend) {
|
|
|
195
209
|
async findByClientId(clientId) {
|
|
196
210
|
return (await backend.load()).find((f) => f.clientId === clientId) ?? null;
|
|
197
211
|
},
|
|
198
|
-
updateFeedback(id, data) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await backend.persist(feedbacks.filter((f) => f.id !== id));
|
|
218
|
-
});
|
|
219
|
-
},
|
|
220
|
-
deleteAllFeedbacks(projectName) {
|
|
221
|
-
return runSerializedMutation(async () => {
|
|
222
|
-
const feedbacks = await backend.load();
|
|
223
|
-
await backend.persist(feedbacks.filter((f) => f.projectName !== projectName));
|
|
224
|
-
});
|
|
225
|
-
},
|
|
212
|
+
updateFeedback: (id, data) => mutate(async (feedbacks) => {
|
|
213
|
+
const current = feedbacks.find((f) => f.id === id);
|
|
214
|
+
if (!current) throw new StoreNotFoundError();
|
|
215
|
+
const updated = {
|
|
216
|
+
...current,
|
|
217
|
+
status: data.status,
|
|
218
|
+
resolvedAt: data.resolvedAt,
|
|
219
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
220
|
+
};
|
|
221
|
+
await backend.persist(feedbacks.map((f) => f === current ? updated : f));
|
|
222
|
+
return updated;
|
|
223
|
+
}),
|
|
224
|
+
deleteFeedback: (id) => mutate(async (feedbacks) => {
|
|
225
|
+
if (!feedbacks.some((f) => f.id === id)) throw new StoreNotFoundError();
|
|
226
|
+
await backend.persist(feedbacks.filter((f) => f.id !== id));
|
|
227
|
+
}),
|
|
228
|
+
deleteAllFeedbacks: (projectName) => mutate(async (feedbacks) => {
|
|
229
|
+
await backend.persist(feedbacks.filter((f) => f.projectName !== projectName));
|
|
230
|
+
}),
|
|
226
231
|
async verifyProjectOwnership(id, projectName) {
|
|
227
232
|
const fb = (await backend.load()).find((f) => f.id === id);
|
|
228
233
|
return fb !== void 0 && fb.projectName === projectName;
|
|
229
234
|
}
|
|
230
235
|
};
|
|
236
|
+
if (!backend.comments) return store;
|
|
237
|
+
return {
|
|
238
|
+
...store,
|
|
239
|
+
addComment: (feedbackId, data) => mutate(async (feedbacks) => {
|
|
240
|
+
const existing = feedbacks.flatMap((f) => f.comments ?? []).find((c) => c.clientId === data.clientId);
|
|
241
|
+
if (existing) return existing;
|
|
242
|
+
const current = feedbacks.find((f) => f.id === feedbackId);
|
|
243
|
+
if (!current) throw new StoreNotFoundError();
|
|
244
|
+
const thread = current.comments ?? [];
|
|
245
|
+
if (data.authorRole === "client" && thread.filter((c) => c.authorRole === "client").length >= MAX_COMMENTS_PER_FEEDBACK) {
|
|
246
|
+
throw new StoreLimitError(`A thread holds at most ${MAX_COMMENTS_PER_FEEDBACK} client comments`);
|
|
247
|
+
}
|
|
248
|
+
const comment = buildCommentRecord(data, { id: backend.generateId(), feedbackId });
|
|
249
|
+
const updated = { ...current, comments: [...thread, comment] };
|
|
250
|
+
await backend.persist(feedbacks.map((f) => f === current ? updated : f));
|
|
251
|
+
return comment;
|
|
252
|
+
}),
|
|
253
|
+
deleteComment: (feedbackId, commentId) => mutate(async (feedbacks) => {
|
|
254
|
+
const current = feedbacks.find((f) => f.id === feedbackId);
|
|
255
|
+
const thread = current?.comments ?? [];
|
|
256
|
+
if (!current || !thread.some((c) => c.id === commentId)) throw new StoreNotFoundError();
|
|
257
|
+
const updated = { ...current, comments: thread.filter((c) => c.id !== commentId) };
|
|
258
|
+
await backend.persist(feedbacks.map((f) => f === current ? updated : f));
|
|
259
|
+
})
|
|
260
|
+
};
|
|
231
261
|
}
|
|
232
262
|
|
|
233
263
|
// src/index.ts
|
|
@@ -239,7 +269,9 @@ var MemoryStore = class {
|
|
|
239
269
|
persist: (next) => {
|
|
240
270
|
this.feedbacks = next;
|
|
241
271
|
},
|
|
242
|
-
generateId: () => `mem-${this.idCounter++}-${Date.now().toString(36)}
|
|
272
|
+
generateId: () => `mem-${this.idCounter++}-${Date.now().toString(36)}`,
|
|
273
|
+
// Records live in memory: comment dates stay Dates.
|
|
274
|
+
comments: true
|
|
243
275
|
});
|
|
244
276
|
createFeedback(data) {
|
|
245
277
|
return this.engine.createFeedback(data);
|
|
@@ -265,6 +297,12 @@ var MemoryStore = class {
|
|
|
265
297
|
verifyProjectOwnership(id, projectName) {
|
|
266
298
|
return this.engine.verifyProjectOwnership(id, projectName);
|
|
267
299
|
}
|
|
300
|
+
addComment(feedbackId, data) {
|
|
301
|
+
return this.engine.addComment(feedbackId, data);
|
|
302
|
+
}
|
|
303
|
+
deleteComment(feedbackId, commentId) {
|
|
304
|
+
return this.engine.deleteComment(feedbackId, commentId);
|
|
305
|
+
}
|
|
268
306
|
/** Remove all data from this store instance. */
|
|
269
307
|
clear() {
|
|
270
308
|
this.feedbacks = [];
|