target-supabase-sdk 0.2.20 → 0.2.22
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/browser.d.ts +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/chunks/{task-queue-CqmIVCBi.js → task-queue-DWFe0rCS.js} +20 -22
- package/dist/chunks/task-queue-DWFe0rCS.js.map +1 -0
- package/dist/node.d.ts +2 -2
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +174 -19
- package/dist/node.js.map +1 -1
- package/dist/shared/feed/feed.build.d.ts +4 -2
- package/dist/shared/feed/feed.build.d.ts.map +1 -1
- package/dist/shared/feed/node/feed.access.d.ts +10 -2
- package/dist/shared/feed/node/feed.access.d.ts.map +1 -1
- package/dist/shared/feed/node/feed.service.d.ts +26 -1
- package/dist/shared/feed/node/feed.service.d.ts.map +1 -1
- package/dist/shared/feed/node/index.d.ts +3 -3
- package/dist/shared/feed/node/index.d.ts.map +1 -1
- package/dist/shared/http/fetch-retry.d.ts +2 -0
- package/dist/shared/http/fetch-retry.d.ts.map +1 -1
- package/dist/shared/media/node/media.service.d.ts +8 -2
- package/dist/shared/media/node/media.service.d.ts.map +1 -1
- package/dist/shared/review/index.d.ts +1 -1
- package/dist/shared/review/index.d.ts.map +1 -1
- package/dist/shared/review/review.build.d.ts +3 -6
- package/dist/shared/review/review.build.d.ts.map +1 -1
- package/dist/shared/review/review.interface.d.ts +2 -2
- package/dist/shared/review/review.query.d.ts +2 -2
- package/dist/shared/review/review.query.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/browser.ts +1 -1
- package/src/node.ts +4 -0
- package/src/shared/feed/README.md +4 -3
- package/src/shared/feed/feed.build.ts +6 -3
- package/src/shared/feed/node/feed.access.ts +33 -6
- package/src/shared/feed/node/feed.service.ts +131 -4
- package/src/shared/feed/node/index.ts +7 -3
- package/src/shared/http/fetch-retry.ts +13 -2
- package/src/shared/media/README.md +4 -4
- package/src/shared/media/node/media.service.ts +77 -10
- package/src/shared/review/README.md +15 -16
- package/src/shared/review/index.ts +1 -1
- package/src/shared/review/review.build.ts +8 -13
- package/src/shared/review/review.interface.ts +2 -2
- package/src/shared/review/review.query.ts +4 -4
- package/dist/chunks/task-queue-CqmIVCBi.js.map +0 -1
|
@@ -28,6 +28,8 @@ export interface FetchRetryOptions {
|
|
|
28
28
|
/** When set, retry/success lines are logged via SDK logger. */
|
|
29
29
|
logger?: LoggerWithScope;
|
|
30
30
|
hint?: string;
|
|
31
|
+
/** Override global `fetch` (e.g. proxied outbound fetch). Ignores `dispatcher`. */
|
|
32
|
+
fetch?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function backoffMs(retryBaseMs: number, attempt: number, response?: Response, maxBackoffMs?: number): number {
|
|
@@ -47,11 +49,20 @@ function backoffMs(retryBaseMs: number, attempt: number, response?: Response, ma
|
|
|
47
49
|
return delayMs;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
async function fetchOnce(
|
|
52
|
+
async function fetchOnce(
|
|
53
|
+
url: string,
|
|
54
|
+
init: RequestInit,
|
|
55
|
+
timeoutMs: number,
|
|
56
|
+
dispatcher?: unknown,
|
|
57
|
+
fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>,
|
|
58
|
+
): Promise<Response> {
|
|
51
59
|
const options: RequestInit & { dispatcher?: unknown } = {
|
|
52
60
|
...init,
|
|
53
61
|
signal: AbortSignal.timeout(timeoutMs),
|
|
54
62
|
};
|
|
63
|
+
if (fetchImpl != null) {
|
|
64
|
+
return fetchImpl(url, options);
|
|
65
|
+
}
|
|
55
66
|
if (dispatcher != null) {
|
|
56
67
|
options.dispatcher = dispatcher;
|
|
57
68
|
}
|
|
@@ -76,7 +87,7 @@ export async function fetchWithRetry(
|
|
|
76
87
|
|
|
77
88
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
78
89
|
try {
|
|
79
|
-
const response = await fetchOnce(url, buildInit(), options.timeoutMs, options.dispatcher);
|
|
90
|
+
const response = await fetchOnce(url, buildInit(), options.timeoutMs, options.dispatcher, options.fetch);
|
|
80
91
|
|
|
81
92
|
if (!response.ok && isRetryableStatus(response.status) && attempt < maxAttempts) {
|
|
82
93
|
const delayMs = backoffMs(retryBaseMs, attempt, response, options.maxBackoffMs);
|
|
@@ -93,10 +93,10 @@ Internal SDK code:
|
|
|
93
93
|
|
|
94
94
|
## Register flow (`registerMediaLink`)
|
|
95
95
|
|
|
96
|
-
1. **Dedup** — `category` + `value` + `name` via `findMediaLink`.
|
|
97
|
-
2. **Availability** — `storageProvider` must equal `localStorageProvider
|
|
98
|
-
3. **Probe** — stream SHA-256 / `stat` / MIME (or directory file count for `image-list`).
|
|
99
|
-
4. **Insert** — `buildMediaLinkDraft` → `postTarget` (race → re-find).
|
|
96
|
+
1. **Dedup** — `category` + `value` + `name` via `findMediaLink`. Local locator: if present → `{ id, created: false }`. HTTP locator: patch `original` when storage/locator differ.
|
|
97
|
+
2. **Availability** — local path: `storageProvider` must equal `localStorageProvider`; `locator` absolute and reachable. HTTP `locator`: require `localLocator` and probe that file on `localStorageProvider`.
|
|
98
|
+
3. **Probe** — stream SHA-256 / `stat` / MIME (or directory file count for `image-list`) from the local file.
|
|
99
|
+
4. **Insert** — `buildMediaLinkDraft` → `postTarget` (race → re-find / HTTP patch). Persisted `locator` may be the HTTP URL.
|
|
100
100
|
|
|
101
101
|
## Env (Node hosts)
|
|
102
102
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { getPossibleTarget, isCreateTargetAlreadyExistsError, postTarget } from "../../../core.api";
|
|
2
|
-
import type
|
|
1
|
+
import { getPossibleTarget, isCreateTargetAlreadyExistsError, postTarget, updateTarget } from "../../../core.api";
|
|
2
|
+
import { CategoryLink, type Link, type LinkDetails } from "../../../link/link.interface";
|
|
3
3
|
import { createLogger } from "../../log/core/create-logger";
|
|
4
|
+
import { isHttpUrl } from "../../utils/fetch-url";
|
|
4
5
|
import { buildMediaLinkDraft } from "../media.build";
|
|
5
6
|
import { isMediaValue, type MediaValue } from "../media.interface";
|
|
6
7
|
import { mediaLinkDedupFilters } from "../media.query";
|
|
@@ -35,6 +36,11 @@ export interface RegisterMediaLinkInput {
|
|
|
35
36
|
locator: string;
|
|
36
37
|
/** This host id for availability (e.g. `process.env.LOCAL_STORAGE_PROVIDER`). */
|
|
37
38
|
localStorageProvider: string;
|
|
39
|
+
/**
|
|
40
|
+
* Local file to probe when `locator` is http(s) (OSS ingest).
|
|
41
|
+
* Availability uses this path + `localStorageProvider`.
|
|
42
|
+
*/
|
|
43
|
+
localLocator?: string;
|
|
38
44
|
description?: string;
|
|
39
45
|
tagList?: string[];
|
|
40
46
|
}
|
|
@@ -44,9 +50,53 @@ export interface RegisterMediaLinkResult {
|
|
|
44
50
|
created: boolean;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
function mediaOriginalFromRow(row: { details?: unknown }): { storageProvider?: string; locator?: string } {
|
|
54
|
+
const details = row.details as LinkDetails | undefined;
|
|
55
|
+
const original = details?.original as { storageProvider?: string; locator?: string } | undefined;
|
|
56
|
+
return original ?? {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function patchMediaOriginal(
|
|
60
|
+
existing: Link,
|
|
61
|
+
storageProvider: string,
|
|
62
|
+
locator: string,
|
|
63
|
+
): Promise<RegisterMediaLinkResult> {
|
|
64
|
+
const original = mediaOriginalFromRow(existing);
|
|
65
|
+
if (original.storageProvider === storageProvider && original.locator === locator) {
|
|
66
|
+
return { id: existing.id, created: false };
|
|
67
|
+
}
|
|
68
|
+
await updateTarget({
|
|
69
|
+
id: existing.id,
|
|
70
|
+
optimisticLockFilterList: [
|
|
71
|
+
{ field: "category", operator: "eq", value: CategoryLink.LINK },
|
|
72
|
+
{ field: "value", operator: "eq", value: existing.value },
|
|
73
|
+
],
|
|
74
|
+
updateFn: (row) => {
|
|
75
|
+
const details = row.details as LinkDetails;
|
|
76
|
+
const prev = (details.original ?? {}) as Record<string, unknown>;
|
|
77
|
+
return {
|
|
78
|
+
details: {
|
|
79
|
+
...details,
|
|
80
|
+
original: {
|
|
81
|
+
...prev,
|
|
82
|
+
storageProvider,
|
|
83
|
+
locator,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
logger.info("media original patched", {
|
|
90
|
+
topic: "register",
|
|
91
|
+
data: { id: existing.id, locator },
|
|
92
|
+
});
|
|
93
|
+
return { id: existing.id, created: false };
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
/**
|
|
48
97
|
* Register a media Link: dedup → availability → probe → `postTarget`.
|
|
49
|
-
* Idempotent when `category`+`value`+`name` already exist.
|
|
98
|
+
* Idempotent when `category`+`value`+`name` already exist (local path).
|
|
99
|
+
* HTTP `locator` + `localLocator`: probe local file, persist remote URL; patch if name exists.
|
|
50
100
|
*
|
|
51
101
|
* **Node-only** — under `shared/media/node/`; do not import from the browser entry.
|
|
52
102
|
*/
|
|
@@ -59,19 +109,33 @@ export async function registerMediaLink(input: RegisterMediaLinkInput): Promise<
|
|
|
59
109
|
throw new Error("registerMediaLink: name is empty");
|
|
60
110
|
}
|
|
61
111
|
|
|
112
|
+
const storageProvider = input.storageProvider.trim();
|
|
113
|
+
const locator = input.locator.trim();
|
|
114
|
+
const localLocator = input.localLocator?.trim() ?? "";
|
|
115
|
+
const httpLocator = isHttpUrl(locator);
|
|
116
|
+
|
|
62
117
|
const existing = await findMediaLink({ value: input.value, name });
|
|
63
|
-
if (existing != null) {
|
|
118
|
+
if (existing != null && !httpLocator) {
|
|
64
119
|
logger.info("media already registered", {
|
|
65
120
|
topic: "dedup",
|
|
66
121
|
data: { id: existing.id, value: input.value, name },
|
|
67
122
|
});
|
|
68
123
|
return { id: existing.id, created: false };
|
|
69
124
|
}
|
|
125
|
+
if (existing != null && httpLocator) {
|
|
126
|
+
return patchMediaOriginal(existing, storageProvider, locator);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const probeStorageProvider = httpLocator ? input.localStorageProvider.trim() : storageProvider;
|
|
130
|
+
const probeLocator = httpLocator ? localLocator : locator;
|
|
131
|
+
if (httpLocator && probeLocator === "") {
|
|
132
|
+
throw new Error("registerMediaLink: localLocator is required when locator is http(s)");
|
|
133
|
+
}
|
|
70
134
|
|
|
71
135
|
const availability = await checkMediaAvailability({
|
|
72
136
|
value: input.value,
|
|
73
|
-
storageProvider:
|
|
74
|
-
locator:
|
|
137
|
+
storageProvider: probeStorageProvider,
|
|
138
|
+
locator: probeLocator,
|
|
75
139
|
localStorageProvider: input.localStorageProvider,
|
|
76
140
|
});
|
|
77
141
|
if (!availability.ok || availability.absolutePath == null) {
|
|
@@ -80,16 +144,16 @@ export async function registerMediaLink(input: RegisterMediaLinkInput): Promise<
|
|
|
80
144
|
|
|
81
145
|
const probed = await probeMediaOriginal({
|
|
82
146
|
value: input.value,
|
|
83
|
-
storageProvider:
|
|
84
|
-
locator:
|
|
147
|
+
storageProvider: probeStorageProvider,
|
|
148
|
+
locator: probeLocator,
|
|
85
149
|
absolutePath: availability.absolutePath,
|
|
86
150
|
});
|
|
87
151
|
|
|
88
152
|
const draft = buildMediaLinkDraft({
|
|
89
153
|
value: input.value,
|
|
90
154
|
name,
|
|
91
|
-
storageProvider: probed.storageProvider,
|
|
92
|
-
locator: probed.locator,
|
|
155
|
+
storageProvider: httpLocator ? storageProvider : probed.storageProvider,
|
|
156
|
+
locator: httpLocator ? locator : probed.locator,
|
|
93
157
|
contentHash: probed.contentHash,
|
|
94
158
|
size: probed.size,
|
|
95
159
|
mimeType: probed.mimeType,
|
|
@@ -117,6 +181,9 @@ export async function registerMediaLink(input: RegisterMediaLinkInput): Promise<
|
|
|
117
181
|
if (isCreateTargetAlreadyExistsError(error)) {
|
|
118
182
|
const raced = await findMediaLink({ value: input.value, name });
|
|
119
183
|
if (raced != null) {
|
|
184
|
+
if (httpLocator) {
|
|
185
|
+
return patchMediaOriginal(raced, storageProvider, locator);
|
|
186
|
+
}
|
|
120
187
|
logger.info("media register race — using existing", {
|
|
121
188
|
topic: "dedup",
|
|
122
189
|
data: { id: raced.id, value: input.value, name },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Review is a **convention on `Link`**, not a new Target category (`category` stays `link`).
|
|
4
4
|
|
|
5
|
-
A **Review** records **one user interaction**
|
|
5
|
+
A **Review** records **one user interaction** (for example: open, play, complete, rate, dismiss). **Each interaction creates a new Review row.** **Many review Links may share the same `Link.name`** — the SDK does not define dedup or idempotent create for reviews.
|
|
6
6
|
|
|
7
7
|
All review code lives under **`src/shared/review/`**. There is no Node-only ingest layer (unlike feed/media register flows) — create drafts on any host and persist with `postTarget` / `createTarget`.
|
|
8
8
|
|
|
@@ -19,12 +19,12 @@ src/shared/review/
|
|
|
19
19
|
|
|
20
20
|
| Consumer import | What you get |
|
|
21
21
|
|-----------------|--------------|
|
|
22
|
-
| `from "target-supabase-sdk"` | Draft builder,
|
|
22
|
+
| `from "target-supabase-sdk"` | Draft builder, list filters |
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
25
|
import {
|
|
26
26
|
buildReviewLinkDraft,
|
|
27
|
-
|
|
27
|
+
reviewLinksForNameFilters,
|
|
28
28
|
} from "target-supabase-sdk";
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -34,12 +34,12 @@ import {
|
|
|
34
34
|
|
|
35
35
|
| Concept | Where it lives |
|
|
36
36
|
|---------|----------------|
|
|
37
|
-
|
|
|
37
|
+
| Grouping key | `Link.name` — caller-defined (often a subject Target id, not required) |
|
|
38
38
|
| Interaction body | `Link.details.original` — caller-defined JSON-serializable payload |
|
|
39
39
|
| Review kind | Fixed `Link.value` / `details.loaderKey` = `"review"` |
|
|
40
40
|
| Labels | Optional `tagList` (e.g. interaction type, client, experiment) |
|
|
41
41
|
|
|
42
|
-
Each persisted Review is its **own** Target row (its own `id`). Repeating the same `
|
|
42
|
+
Each persisted Review is its **own** Target row (its own `id`). Repeating the same `name` is expected when listing interaction history under one grouping key.
|
|
43
43
|
|
|
44
44
|
`details.original` is intentionally **untyped** (`unknown`) in the SDK so products can store scores, dwell time, UI state, model output, etc., without a shared schema in this package.
|
|
45
45
|
|
|
@@ -51,8 +51,7 @@ Built via `buildReviewLinkDraft` → `buildLinkTargetDraft`.
|
|
|
51
51
|
|
|
52
52
|
| Create field | Required | Link field |
|
|
53
53
|
|--------------|----------|------------|
|
|
54
|
-
| `
|
|
55
|
-
| `refId` | yes | `name` — subject Target id |
|
|
54
|
+
| `name` | yes | `name` |
|
|
56
55
|
| `original` | yes | `details.original` |
|
|
57
56
|
| `tagList` | no | `tagList` (default `[]`) |
|
|
58
57
|
| `description` | no | `details.description` (default `""`) |
|
|
@@ -61,8 +60,9 @@ Fixed at build time (not on create input):
|
|
|
61
60
|
|
|
62
61
|
| Link field | Value |
|
|
63
62
|
|------------|--------|
|
|
63
|
+
| `value` | `"review"` |
|
|
64
64
|
| `category` | `link` |
|
|
65
|
-
| `details.loaderKey` | `"review"`
|
|
65
|
+
| `details.loaderKey` | `"review"` |
|
|
66
66
|
| `details.manifestVersion` | `0` |
|
|
67
67
|
| `details.preview` | `""` |
|
|
68
68
|
|
|
@@ -75,8 +75,7 @@ import { buildReviewLinkDraft } from "target-supabase-sdk";
|
|
|
75
75
|
import { postTarget } from "target-supabase-sdk"; // or your create API
|
|
76
76
|
|
|
77
77
|
const draft = buildReviewLinkDraft({
|
|
78
|
-
|
|
79
|
-
refId: subjectTargetId,
|
|
78
|
+
name: subjectTargetId,
|
|
80
79
|
original: {
|
|
81
80
|
action: "complete",
|
|
82
81
|
progress: 1,
|
|
@@ -88,23 +87,23 @@ const draft = buildReviewLinkDraft({
|
|
|
88
87
|
await postTarget(draft);
|
|
89
88
|
```
|
|
90
89
|
|
|
91
|
-
Every interaction: build a new draft (same `
|
|
90
|
+
Every interaction: build a new draft (same `name` is fine) and insert again.
|
|
92
91
|
|
|
93
92
|
---
|
|
94
93
|
|
|
95
94
|
## Query
|
|
96
95
|
|
|
97
|
-
List review Links
|
|
96
|
+
List review Links sharing a `Link.name`:
|
|
98
97
|
|
|
99
98
|
```typescript
|
|
100
|
-
import {
|
|
99
|
+
import { reviewLinksForNameFilters, scanTargetList } from "target-supabase-sdk";
|
|
101
100
|
|
|
102
101
|
const reviews = await scanTargetList({
|
|
103
|
-
filters:
|
|
102
|
+
filters: reviewLinksForNameFilters(subjectTargetId),
|
|
104
103
|
});
|
|
105
104
|
```
|
|
106
105
|
|
|
107
|
-
Filters: `category=link`, `value=review`, `name
|
|
106
|
+
Filters: `category=link`, `value=review`, `name=<trimmed name>`. Use pagination / `scanTargetList` when history is long.
|
|
108
107
|
|
|
109
108
|
There is **no** SDK helper for dedup or “find existing review” — callers always create new rows unless they add their own policy.
|
|
110
109
|
|
|
@@ -114,7 +113,7 @@ There is **no** SDK helper for dedup or “find existing review” — callers a
|
|
|
114
113
|
|
|
115
114
|
```typescript
|
|
116
115
|
if (link.category === "link" && link.value === "review") {
|
|
117
|
-
const
|
|
116
|
+
const groupingKey = link.name;
|
|
118
117
|
const payload = link.details.original;
|
|
119
118
|
}
|
|
120
119
|
```
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { TargetDraft } from "../../core.interface";
|
|
2
2
|
import { buildLinkTargetDraft } from "../../link/link.build";
|
|
3
3
|
import type { Link } from "../../link/link.interface";
|
|
4
|
-
import {
|
|
4
|
+
import { REVIEW_VALUE } from "./review.interface";
|
|
5
5
|
|
|
6
6
|
export interface BuildReviewLinkDraftInput {
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
/** Associated Target id — written to `Link.name`. */
|
|
10
|
-
refId: string;
|
|
7
|
+
/** Written to `Link.name` (caller-defined; often a subject Target id). */
|
|
8
|
+
name: string;
|
|
11
9
|
/** Interaction payload — written to `Link.details.original`. */
|
|
12
10
|
original: unknown;
|
|
13
11
|
tagList?: string[];
|
|
@@ -16,19 +14,16 @@ export interface BuildReviewLinkDraftInput {
|
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* Assemble a review {@link Link} draft (`category=link`, `loaderKey === value === "review"`).
|
|
19
|
-
* Does not validate `
|
|
17
|
+
* Does not validate `name` or write to Supabase.
|
|
20
18
|
*/
|
|
21
19
|
export function buildReviewLinkDraft(input: BuildReviewLinkDraftInput): TargetDraft<Link> {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const refId = input.refId.trim();
|
|
26
|
-
if (refId === "") {
|
|
27
|
-
throw new Error("buildReviewLinkDraft: refId is empty");
|
|
20
|
+
const name = input.name.trim();
|
|
21
|
+
if (name === "") {
|
|
22
|
+
throw new Error("buildReviewLinkDraft: name is empty");
|
|
28
23
|
}
|
|
29
24
|
|
|
30
25
|
return buildLinkTargetDraft({
|
|
31
|
-
name
|
|
26
|
+
name,
|
|
32
27
|
value: REVIEW_VALUE,
|
|
33
28
|
description: input.description ?? "",
|
|
34
29
|
preview: "",
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Review Link protocol — Link rows with `value` / `loaderKey` === {@link REVIEW_VALUE}.
|
|
3
3
|
* No separate Target category; category remains `link`.
|
|
4
4
|
*
|
|
5
|
-
* A Review records one user interaction
|
|
6
|
-
* Many review Links may share the same
|
|
5
|
+
* A Review records one user interaction; `Link.name` is caller-defined (often a subject Target id).
|
|
6
|
+
* Many review Links may share the same `name`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const REVIEW_VALUE = "review" as const;
|
|
@@ -3,14 +3,14 @@ import { CategoryLink } from "../../link/link.interface";
|
|
|
3
3
|
import { REVIEW_VALUE } from "./review.interface";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Filters for
|
|
6
|
+
* Filters for review Links with `Link.name === name`.
|
|
7
7
|
* Many rows may match — the SDK does not define dedup for reviews.
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
10
|
-
const
|
|
9
|
+
export function reviewLinksForNameFilters(name: string): QueryFilter[] {
|
|
10
|
+
const trimmed = name.trim();
|
|
11
11
|
return [
|
|
12
12
|
{ field: "category", operator: "eq", value: CategoryLink.LINK },
|
|
13
13
|
{ field: "value", operator: "eq", value: REVIEW_VALUE },
|
|
14
|
-
{ field: "name", operator: "eq", value:
|
|
14
|
+
{ field: "name", operator: "eq", value: trimmed },
|
|
15
15
|
];
|
|
16
16
|
}
|