target-supabase-sdk 0.2.23 → 0.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/browser.d.ts +3 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -2
- package/dist/browser.js.map +1 -1
- package/dist/chunks/{enable-C7lFooTG.js → enable-DkiKPHDl.js} +20 -121
- package/dist/chunks/enable-DkiKPHDl.js.map +1 -0
- package/dist/chunks/{list.api-BzbTJBsM.js → list.api-B-aM4vzv.js} +46 -10
- package/dist/chunks/list.api-B-aM4vzv.js.map +1 -0
- package/dist/chunks/{task-queue-Ba8S21I6.js → task-queue-BYfO0Nzm.js} +212 -28
- package/dist/chunks/task-queue-BYfO0Nzm.js.map +1 -0
- package/dist/link/index.d.ts +2 -0
- package/dist/link/index.d.ts.map +1 -1
- package/dist/link/link.query.d.ts +9 -0
- package/dist/link/link.query.d.ts.map +1 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +157 -27
- package/dist/node.js.map +1 -1
- package/dist/preload.js +3 -3
- package/dist/shared/feed/feed.query.d.ts.map +1 -1
- package/dist/shared/idea/idea.build.d.ts +35 -0
- package/dist/shared/idea/idea.build.d.ts.map +1 -0
- package/dist/shared/idea/idea.interface.d.ts +23 -0
- package/dist/shared/idea/idea.interface.d.ts.map +1 -0
- package/dist/shared/idea/idea.normalize.d.ts +3 -0
- package/dist/shared/idea/idea.normalize.d.ts.map +1 -0
- package/dist/shared/idea/idea.parse.d.ts +3 -0
- package/dist/shared/idea/idea.parse.d.ts.map +1 -0
- package/dist/shared/idea/idea.query.d.ts +6 -0
- package/dist/shared/idea/idea.query.d.ts.map +1 -0
- package/dist/shared/idea/idea.schema.d.ts +60 -0
- package/dist/shared/idea/idea.schema.d.ts.map +1 -0
- package/dist/shared/idea/index.d.ts +15 -0
- package/dist/shared/idea/index.d.ts.map +1 -0
- package/dist/shared/idea/node/idea.hash.d.ts +3 -0
- package/dist/shared/idea/node/idea.hash.d.ts.map +1 -0
- package/dist/shared/idea/node/idea.locator.d.ts +9 -0
- package/dist/shared/idea/node/idea.locator.d.ts.map +1 -0
- package/dist/shared/idea/node/index.d.ts +3 -0
- package/dist/shared/idea/node/index.d.ts.map +1 -0
- package/dist/shared/log/core/log-batch.d.ts +4 -4
- package/dist/shared/log/core/log-batch.d.ts.map +1 -1
- package/dist/shared/log/index.d.ts +2 -1
- package/dist/shared/log/index.d.ts.map +1 -1
- package/dist/shared/log/spool/collector.d.ts +1 -1
- package/dist/shared/log/spool/collector.d.ts.map +1 -1
- package/dist/shared/log/spool/file.d.ts +2 -8
- package/dist/shared/log/spool/file.d.ts.map +1 -1
- package/dist/shared/log/spool/writer.d.ts.map +1 -1
- package/dist/shared/log/upload/flush.d.ts.map +1 -1
- package/dist/shared/log/upload/interface.d.ts +3 -11
- package/dist/shared/log/upload/interface.d.ts.map +1 -1
- package/dist/shared/media/media.query.d.ts.map +1 -1
- package/dist/shared/review/review.query.d.ts.map +1 -1
- package/dist/shared/utils/config-path.utils.d.ts +1 -1
- package/dist/shared/utils/postgrest.utils.d.ts +1 -1
- package/dist/task/index.d.ts +1 -1
- package/dist/task/local-task-registry.d.ts +1 -1
- package/dist/trigger/trigger.api.d.ts +6 -1
- package/dist/trigger/trigger.api.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/browser.ts +23 -2
- package/src/link/index.ts +2 -0
- package/src/link/link.query.ts +18 -0
- package/src/node.ts +8 -0
- package/src/shared/feed/README.md +1 -1
- package/src/shared/feed/feed.query.ts +2 -7
- package/src/shared/idea/README.md +82 -0
- package/src/shared/idea/VALIDATION-MATRIX.md +69 -0
- package/src/shared/idea/idea.build.ts +160 -0
- package/src/shared/idea/idea.interface.ts +34 -0
- package/src/shared/idea/idea.normalize.ts +17 -0
- package/src/shared/idea/idea.parse.ts +16 -0
- package/src/shared/idea/idea.query.ts +16 -0
- package/src/shared/idea/idea.schema.ts +32 -0
- package/src/shared/idea/index.ts +21 -0
- package/src/shared/idea/node/idea.hash.ts +6 -0
- package/src/shared/idea/node/idea.locator.ts +61 -0
- package/src/shared/idea/node/index.ts +7 -0
- package/src/shared/log/README.md +2 -3
- package/src/shared/log/core/log-batch.ts +12 -10
- package/src/shared/log/index.ts +2 -1
- package/src/shared/log/spool/collector.ts +46 -20
- package/src/shared/log/spool/file.ts +12 -25
- package/src/shared/log/spool/writer.ts +2 -7
- package/src/shared/log/upload/flush.ts +23 -38
- package/src/shared/log/upload/interface.ts +3 -12
- package/src/shared/media/README.md +1 -1
- package/src/shared/media/media.query.ts +2 -7
- package/src/shared/review/review.query.ts +2 -7
- package/src/shared/utils/config-path.utils.ts +1 -1
- package/src/shared/utils/postgrest.utils.ts +1 -1
- package/src/task/index.ts +1 -1
- package/src/task/local-task-registry.ts +1 -1
- package/src/trigger/trigger.api.ts +6 -1
- package/dist/chunks/enable-C7lFooTG.js.map +0 -1
- package/dist/chunks/list.api-BzbTJBsM.js.map +0 -1
- package/dist/chunks/task-queue-Ba8S21I6.js.map +0 -1
- package/dist/resource/resource.interface.d.ts +0 -20
- package/dist/resource/resource.interface.d.ts.map +0 -1
- package/dist/shared/log/upload/batch-id.d.ts +0 -13
- package/dist/shared/log/upload/batch-id.d.ts.map +0 -1
- package/dist/task/task.utils.d.ts +0 -1
- package/dist/task/task.utils.d.ts.map +0 -1
- package/src/resource/resource.interface.ts +0 -37
- package/src/shared/log/upload/batch-id.ts +0 -43
- package/src/task/task.utils.ts +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Idea Links (`shared/idea`)
|
|
2
|
+
|
|
3
|
+
Ideas are a **convention on `Link`**, not a new Target category (`category` stays `link`).
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
src/shared/idea/
|
|
7
|
+
index.ts ← browser-safe barrel ONLY (never export ./node)
|
|
8
|
+
idea.interface.ts
|
|
9
|
+
idea.schema.ts
|
|
10
|
+
idea.build.ts
|
|
11
|
+
idea.normalize.ts
|
|
12
|
+
idea.parse.ts
|
|
13
|
+
idea.query.ts
|
|
14
|
+
README.md
|
|
15
|
+
VALIDATION-MATRIX.md
|
|
16
|
+
node/ ← Node-only (hash + temp download URL helpers)
|
|
17
|
+
index.ts
|
|
18
|
+
idea.hash.ts
|
|
19
|
+
idea.locator.ts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Variants
|
|
23
|
+
|
|
24
|
+
| Kind | `value` | `name` | `original.content` | `original.url` |
|
|
25
|
+
|------|---------|--------|--------------------|----------------|
|
|
26
|
+
| Text note | `idea` | Title (caller) | Body text | Source page URL, etc. |
|
|
27
|
+
| Clipboard image | `idea` | **`idea.image`** (fixed) | SHA-256 hex of bytes | Temp download URL |
|
|
28
|
+
|
|
29
|
+
Filter rows with **`value=idea`** only — do not query `details.loaderKey`. When assembling drafts, set **`loaderKey === value === "idea"`**.
|
|
30
|
+
|
|
31
|
+
There is **no content-hash dedup** for image ideas.
|
|
32
|
+
|
|
33
|
+
## Package entry split
|
|
34
|
+
|
|
35
|
+
| Consumer import | What you get | Source |
|
|
36
|
+
|-----------------|--------------|--------|
|
|
37
|
+
| `from "target-supabase-sdk"` | Draft builders, schemas, parse, list filters | `shared/idea/index.ts` → `browser.ts` |
|
|
38
|
+
| `from "target-supabase-sdk/node"` | + `hashIdeaPasteImage`, temp download locators | `shared/idea/node/` → `node.ts` |
|
|
39
|
+
|
|
40
|
+
**Do not** re-export `./node` from `index.ts` / `browser.ts`.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import {
|
|
44
|
+
buildIdeaLinkDraft,
|
|
45
|
+
buildIdeaImageLinkDraft,
|
|
46
|
+
ideaLinkValueFilters,
|
|
47
|
+
IDEA_LINK_VALUE,
|
|
48
|
+
IDEA_IMAGE_LINK_NAME,
|
|
49
|
+
ideaCreateSchema,
|
|
50
|
+
parseIdeaOriginal,
|
|
51
|
+
} from "target-supabase-sdk";
|
|
52
|
+
|
|
53
|
+
import {
|
|
54
|
+
hashIdeaPasteImage,
|
|
55
|
+
buildIdeaTempDownloadLocator,
|
|
56
|
+
buildIdeaPasteRelativePath,
|
|
57
|
+
} from "target-supabase-sdk/node";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Quick-note (chrome extension) should wrap `buildIdeaLinkDraft` **locally** with extension tags — not via a dedicated SDK export.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Link field rules
|
|
65
|
+
|
|
66
|
+
| Field | Rule |
|
|
67
|
+
|-------|------|
|
|
68
|
+
| `value` | Always `idea` |
|
|
69
|
+
| `details.loaderKey` | Always `idea` |
|
|
70
|
+
| Text `name` | Title; max 500; must not be `idea.image` |
|
|
71
|
+
| Image `name` | Fixed `idea.image` via `buildIdeaImageLinkDraft` |
|
|
72
|
+
| `description` / `preview` | Default `""` |
|
|
73
|
+
| `tagList` | User/system tags; image default `[ ".raw" ]` |
|
|
74
|
+
|
|
75
|
+
See [VALIDATION-MATRIX.md](./VALIDATION-MATRIX.md) for limits across Zod, builders, and HTTP.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Related
|
|
80
|
+
|
|
81
|
+
- upload-service: `.cursor/skills/idea-link/SKILL.md`
|
|
82
|
+
- `.cursor/skills/link-conventions/SKILL.md`
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Idea Link validation matrix
|
|
2
|
+
|
|
3
|
+
Single reference for **where** each constraint is enforced. Limits match upload-service `POST/PATCH /v0/upload/idea` and paste-image behavior.
|
|
4
|
+
|
|
5
|
+
Legend: **Z** = `ideaOriginalSchema` / `ideaCreateSchema` / `ideaUpdateSchema` (Zod); **B** = `buildIdeaLinkDraft` / `buildIdeaImageLinkDraft`; **H** = upload-service HTTP handler (Zod on request body); **P** = paste-image route (MIME, size, empty buffer).
|
|
6
|
+
|
|
7
|
+
## Row identity
|
|
8
|
+
|
|
9
|
+
| Rule | Z | B | H | Notes |
|
|
10
|
+
|------|---|---|---|-------|
|
|
11
|
+
| `category=link` | — | — | — | Set by `createTarget` / SDK category, not in draft builder |
|
|
12
|
+
| `value=idea` | — | ✓ | — | Fixed in builders |
|
|
13
|
+
| `loaderKey=idea` | — | ✓ | — | Same as `value` |
|
|
14
|
+
| Text `name` non-empty | ✓ create | ✓ | ✓ | `ideaCreateSchema` / builder |
|
|
15
|
+
| Text `name` max 500 | ✓ create/update | ✓ | ✓ | |
|
|
16
|
+
| Text `name` ≠ `idea.image` | — | ✓ | — | Use `buildIdeaImageLinkDraft` for images |
|
|
17
|
+
| Image `name=idea.image` | — | ✓ | — | Fixed in `buildIdeaImageLinkDraft` |
|
|
18
|
+
| No title uniqueness / dedup | — | — | — | By design |
|
|
19
|
+
|
|
20
|
+
## `details.original`
|
|
21
|
+
|
|
22
|
+
| Field | Z | B | H | Limit / rule |
|
|
23
|
+
|-------|---|---|---|----------------|
|
|
24
|
+
| `content` | ✓ min 1, max 100_000 | ✓ same | ✓ create/update | Text body or image SHA-256 hex |
|
|
25
|
+
| `reviewList` | ✓ max 32 items, each 1–8000 | ✓ | ✓ | Normalized (trim, dedupe) |
|
|
26
|
+
| `url` | ✓ max 2000, default `""` | ✓ | ✓ | Image: temp download URL required in builder |
|
|
27
|
+
| `heat` | ✓ int ≥ 0, default 0 | ✓ floor 0 | — | PATCH +1 is upload-service only |
|
|
28
|
+
| Image `reviewList` | — | ✓ `[]` | — | Builder fixed |
|
|
29
|
+
| Image `heat` | — | ✓ `0` | — | Builder fixed |
|
|
30
|
+
|
|
31
|
+
## `tagList`
|
|
32
|
+
|
|
33
|
+
| Rule | Z create | B text | B image | Notes |
|
|
34
|
+
|------|----------|--------|---------|-------|
|
|
35
|
+
| Max 32 tags | ✓ | ✓ | ✓ | |
|
|
36
|
+
| Each tag 1–128 chars | ✓ | ✓ | — | Builder checks length on text tags |
|
|
37
|
+
| Default image tags | — | — | ✓ `[ ".raw" ]` if omitted | |
|
|
38
|
+
| Quick-note `.raw` | — | — | — | Extension local wrapper only |
|
|
39
|
+
|
|
40
|
+
## Paste-image (bytes → Link)
|
|
41
|
+
|
|
42
|
+
| Rule | P | B image | Node hash/locator |
|
|
43
|
+
|------|---|---------|-------------------|
|
|
44
|
+
| MIME whitelist | ✓ | — | — |
|
|
45
|
+
| Non-empty buffer | ✓ | — | — |
|
|
46
|
+
| Max file size | ✓ `TEMP_FS_MAX_FILE_SIZE` | — | — |
|
|
47
|
+
| Optional paste `content` field | — | — | upload-service: caption → `original.content`; empty → hash |
|
|
48
|
+
| Content hash (file path) | — | — | `hashIdeaPasteImage` + `buildIdeaPasteRelativePath` |
|
|
49
|
+
| Stored `original.content` | ✓ min 1 | ✓ | user text or hash fallback |
|
|
50
|
+
| Temp path `idea/paste/{hash}.{ext}` | — | — | `buildIdeaPasteRelativePath` |
|
|
51
|
+
| `original.url` temp download | — | ✓ non-empty URL | `buildIdeaTempDownloadLocator` |
|
|
52
|
+
| Content-hash dedup before create | — | — | **Not implemented** |
|
|
53
|
+
|
|
54
|
+
## Query / read
|
|
55
|
+
|
|
56
|
+
| Rule | SDK | upload-service |
|
|
57
|
+
|------|-----|----------------|
|
|
58
|
+
| Filter by `value=idea` | `ideaLinkValueFilters` | list/get/delete |
|
|
59
|
+
| Image-only list | `ideaImageLinkFilters` | `linkName=idea.image` |
|
|
60
|
+
| Parse row `original` | `parseIdeaOriginal` | `toIdeaView` throws if invalid |
|
|
61
|
+
| Search `name` / `content` ilike | — | list API only |
|
|
62
|
+
|
|
63
|
+
## Error style
|
|
64
|
+
|
|
65
|
+
| Layer | Behavior |
|
|
66
|
+
|-------|----------|
|
|
67
|
+
| Zod (H / optional client) | Field errors in HTTP 400 |
|
|
68
|
+
| `buildIdea*Draft` | `throw new Error("buildIdeaLinkDraft: …")` |
|
|
69
|
+
| `parseIdeaOriginal` | Returns `null` (no throw) |
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { TargetDraft } from "../../core.interface";
|
|
2
|
+
import { buildLinkTargetDraft } from "../../link/link.build";
|
|
3
|
+
import type { Link } from "../../link/link.interface";
|
|
4
|
+
import {
|
|
5
|
+
IDEA_IMAGE_LINK_NAME,
|
|
6
|
+
IDEA_IMAGE_TAG_RAW,
|
|
7
|
+
IDEA_LINK_VALUE,
|
|
8
|
+
type IdeaOriginal,
|
|
9
|
+
isIdeaImageLinkName,
|
|
10
|
+
} from "./idea.interface";
|
|
11
|
+
import { normalizeStringList } from "./idea.normalize";
|
|
12
|
+
|
|
13
|
+
export interface BuildIdeaLinkDraftInput {
|
|
14
|
+
/** Article title — Link.name (not used for image ideas). */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Body or image content hash — `details.original.content`. */
|
|
17
|
+
content: string;
|
|
18
|
+
reviewList?: readonly string[];
|
|
19
|
+
url?: string;
|
|
20
|
+
heat?: number;
|
|
21
|
+
tagList?: readonly string[];
|
|
22
|
+
description?: string;
|
|
23
|
+
preview?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BuildIdeaImageLinkDraftInput {
|
|
27
|
+
/** SHA-256 hex of image bytes — stored in `details.original.content`. */
|
|
28
|
+
contentHash: string;
|
|
29
|
+
/** Temp download or other absolute URL — `details.original.url`. */
|
|
30
|
+
url: string;
|
|
31
|
+
heat?: number;
|
|
32
|
+
tagList?: readonly string[];
|
|
33
|
+
description?: string;
|
|
34
|
+
preview?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveIdeaOriginal(input: {
|
|
38
|
+
content: string;
|
|
39
|
+
reviewList?: readonly string[];
|
|
40
|
+
url?: string;
|
|
41
|
+
heat?: number;
|
|
42
|
+
}): IdeaOriginal {
|
|
43
|
+
const content = input.content.trim();
|
|
44
|
+
if (content === "") {
|
|
45
|
+
throw new Error("buildIdeaLinkDraft: content is empty");
|
|
46
|
+
}
|
|
47
|
+
if (content.length > 100_000) {
|
|
48
|
+
throw new Error("buildIdeaLinkDraft: content exceeds 100000 characters");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const url = input.url?.trim() ?? "";
|
|
52
|
+
if (url.length > 2000) {
|
|
53
|
+
throw new Error("buildIdeaLinkDraft: url exceeds 2000 characters");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const reviewList = normalizeStringList(input.reviewList);
|
|
57
|
+
if (reviewList.length > 32) {
|
|
58
|
+
throw new Error("buildIdeaLinkDraft: reviewList exceeds 32 entries");
|
|
59
|
+
}
|
|
60
|
+
for (const entry of reviewList) {
|
|
61
|
+
if (entry.length > 8000) {
|
|
62
|
+
throw new Error("buildIdeaLinkDraft: reviewList entry exceeds 8000 characters");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content,
|
|
68
|
+
reviewList,
|
|
69
|
+
url,
|
|
70
|
+
heat: Math.max(0, Math.floor(input.heat ?? 0)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Assemble a text idea {@link Link} draft (`category=link`, `loaderKey === value === "idea"`).
|
|
76
|
+
* Does not write to Supabase or enforce title uniqueness.
|
|
77
|
+
*/
|
|
78
|
+
export function buildIdeaLinkDraft(input: BuildIdeaLinkDraftInput): TargetDraft<Link> {
|
|
79
|
+
const name = input.name.trim();
|
|
80
|
+
if (name === "") {
|
|
81
|
+
throw new Error("buildIdeaLinkDraft: name is empty");
|
|
82
|
+
}
|
|
83
|
+
if (name.length > 500) {
|
|
84
|
+
throw new Error("buildIdeaLinkDraft: name exceeds 500 characters");
|
|
85
|
+
}
|
|
86
|
+
if (isIdeaImageLinkName(name)) {
|
|
87
|
+
throw new Error("buildIdeaLinkDraft: name is reserved for image ideas; use buildIdeaImageLinkDraft");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tagList = normalizeStringList(input.tagList);
|
|
91
|
+
if (tagList.length > 32) {
|
|
92
|
+
throw new Error("buildIdeaLinkDraft: tagList exceeds 32 entries");
|
|
93
|
+
}
|
|
94
|
+
for (const tag of tagList) {
|
|
95
|
+
if (tag.length > 128) {
|
|
96
|
+
throw new Error("buildIdeaLinkDraft: tag exceeds 128 characters");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const original = resolveIdeaOriginal({
|
|
101
|
+
content: input.content,
|
|
102
|
+
reviewList: input.reviewList,
|
|
103
|
+
url: input.url,
|
|
104
|
+
heat: input.heat,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return buildLinkTargetDraft({
|
|
108
|
+
name,
|
|
109
|
+
value: IDEA_LINK_VALUE,
|
|
110
|
+
description: input.description ?? "",
|
|
111
|
+
preview: input.preview ?? "",
|
|
112
|
+
loaderKey: IDEA_LINK_VALUE,
|
|
113
|
+
tagList,
|
|
114
|
+
original,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Assemble a clipboard-image idea Link (`name=idea.image`, `value=idea`).
|
|
120
|
+
* Caller stores bytes and supplies hash + temp URL; no dedup by content hash.
|
|
121
|
+
*/
|
|
122
|
+
export function buildIdeaImageLinkDraft(input: BuildIdeaImageLinkDraftInput): TargetDraft<Link> {
|
|
123
|
+
const contentHash = input.contentHash.trim();
|
|
124
|
+
if (contentHash === "") {
|
|
125
|
+
throw new Error("buildIdeaImageLinkDraft: contentHash is empty");
|
|
126
|
+
}
|
|
127
|
+
if (contentHash.length > 100_000) {
|
|
128
|
+
throw new Error("buildIdeaImageLinkDraft: contentHash exceeds 100000 characters");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const url = input.url.trim();
|
|
132
|
+
if (url === "") {
|
|
133
|
+
throw new Error("buildIdeaImageLinkDraft: url is empty");
|
|
134
|
+
}
|
|
135
|
+
if (url.length > 2000) {
|
|
136
|
+
throw new Error("buildIdeaImageLinkDraft: url exceeds 2000 characters");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tagList = normalizeStringList(input.tagList ?? [IDEA_IMAGE_TAG_RAW]);
|
|
140
|
+
if (tagList.length === 0) {
|
|
141
|
+
throw new Error("buildIdeaImageLinkDraft: tagList is empty");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const original: IdeaOriginal = {
|
|
145
|
+
content: contentHash,
|
|
146
|
+
reviewList: [],
|
|
147
|
+
url,
|
|
148
|
+
heat: Math.max(0, Math.floor(input.heat ?? 0)),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return buildLinkTargetDraft({
|
|
152
|
+
name: IDEA_IMAGE_LINK_NAME,
|
|
153
|
+
value: IDEA_LINK_VALUE,
|
|
154
|
+
description: input.description ?? "",
|
|
155
|
+
preview: input.preview ?? "",
|
|
156
|
+
loaderKey: IDEA_LINK_VALUE,
|
|
157
|
+
tagList,
|
|
158
|
+
original,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idea Link protocol — Link rows with `value` / `loaderKey` === {@link IDEA_LINK_VALUE}.
|
|
3
|
+
* Clipboard images use fixed `name` {@link IDEA_IMAGE_LINK_NAME} (not a separate value).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const IDEA_LINK_VALUE = "idea" as const;
|
|
7
|
+
|
|
8
|
+
/** Discriminator stored in both `Link.value` and `details.loaderKey`. */
|
|
9
|
+
export type IdeaLinkValue = typeof IDEA_LINK_VALUE;
|
|
10
|
+
|
|
11
|
+
/** Link.name for clipboard-image ideas (distinct from title-based text ideas). */
|
|
12
|
+
export const IDEA_IMAGE_LINK_NAME = "idea.image" as const;
|
|
13
|
+
|
|
14
|
+
/** Default tag for raw captures (quick-note, paste-image). Leading `.` = system tag. */
|
|
15
|
+
export const IDEA_IMAGE_TAG_RAW = ".raw" as const;
|
|
16
|
+
|
|
17
|
+
/** Temp sandbox prefix for pasted images (`TEMP_FS_ROOT` on upload-service). */
|
|
18
|
+
export const IDEA_IMAGE_TEMP_DIR = "idea/paste" as const;
|
|
19
|
+
|
|
20
|
+
/** `Link.details.original` for idea Links (text and image). */
|
|
21
|
+
export interface IdeaOriginal {
|
|
22
|
+
content: string;
|
|
23
|
+
reviewList: string[];
|
|
24
|
+
url: string;
|
|
25
|
+
heat: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isIdeaLinkValue(value: string): value is IdeaLinkValue {
|
|
29
|
+
return value === IDEA_LINK_VALUE;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isIdeaImageLinkName(name: string): boolean {
|
|
33
|
+
return name.trim() === IDEA_IMAGE_LINK_NAME;
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Trim, dedupe, and drop empty strings from tag/review lists. */
|
|
2
|
+
export function normalizeStringList(raw: readonly string[] | undefined | null): string[] {
|
|
3
|
+
if (raw == null || raw.length === 0) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const seen = new Set<string>();
|
|
7
|
+
const out: string[] = [];
|
|
8
|
+
for (const item of raw) {
|
|
9
|
+
const text = item.trim();
|
|
10
|
+
if (text === "" || seen.has(text)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
seen.add(text);
|
|
14
|
+
out.push(text);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IdeaOriginal } from "./idea.interface";
|
|
2
|
+
import { normalizeStringList } from "./idea.normalize";
|
|
3
|
+
import { ideaOriginalSchema } from "./idea.schema";
|
|
4
|
+
|
|
5
|
+
export function parseIdeaOriginal(original: unknown): IdeaOriginal | null {
|
|
6
|
+
const parsed = ideaOriginalSchema.safeParse(original);
|
|
7
|
+
if (!parsed.success) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
content: parsed.data.content,
|
|
12
|
+
reviewList: normalizeStringList(parsed.data.reviewList),
|
|
13
|
+
url: parsed.data.url,
|
|
14
|
+
heat: parsed.data.heat,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { QueryFilter } from "../../core.interface";
|
|
2
|
+
import { CategoryLink } from "../../link/link.interface";
|
|
3
|
+
import { IDEA_IMAGE_LINK_NAME, IDEA_LINK_VALUE } from "./idea.interface";
|
|
4
|
+
|
|
5
|
+
/** List/get filters: `category=link` and `value=idea` (do not filter on `details.loaderKey`). */
|
|
6
|
+
export function ideaLinkValueFilters(): QueryFilter[] {
|
|
7
|
+
return [
|
|
8
|
+
{ field: "category", operator: "eq", value: CategoryLink.LINK },
|
|
9
|
+
{ field: "value", operator: "eq", value: IDEA_LINK_VALUE },
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Clipboard-image ideas only (`name=idea.image`). */
|
|
14
|
+
export function ideaImageLinkFilters(): QueryFilter[] {
|
|
15
|
+
return [...ideaLinkValueFilters(), { field: "name", operator: "eq", value: IDEA_IMAGE_LINK_NAME }];
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Runtime shape of `Link.details.original` for value=idea. */
|
|
4
|
+
export const ideaOriginalSchema = z.object({
|
|
5
|
+
content: z.string().trim().min(1).max(100_000),
|
|
6
|
+
reviewList: z.array(z.string().trim().min(1).max(8000)).max(32).default([]),
|
|
7
|
+
url: z.string().trim().max(2000).default(""),
|
|
8
|
+
heat: z.number().int().min(0).default(0),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const ideaTagSchema = z.string().trim().min(1).max(128);
|
|
12
|
+
|
|
13
|
+
/** HTTP / API create body limits (upload-service `POST /v0/upload/idea`). */
|
|
14
|
+
export const ideaCreateSchema = z.object({
|
|
15
|
+
name: z.string().trim().min(1, "name is required").max(500),
|
|
16
|
+
content: z.string().trim().min(1, "content is required").max(100_000),
|
|
17
|
+
reviewList: z.array(z.string().trim().min(1).max(8000)).max(32).default([]),
|
|
18
|
+
url: z.string().trim().max(2000).default(""),
|
|
19
|
+
tagList: z.array(ideaTagSchema).max(32).default([]),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type IdeaCreateSchemaInput = z.infer<typeof ideaCreateSchema>;
|
|
23
|
+
|
|
24
|
+
export const ideaUpdateSchema = z.object({
|
|
25
|
+
name: z.string().trim().min(1).max(500).optional(),
|
|
26
|
+
content: z.string().trim().min(1).max(100_000).optional(),
|
|
27
|
+
reviewList: z.array(z.string().trim().min(1).max(8000)).max(32).optional(),
|
|
28
|
+
url: z.string().trim().max(2000).optional(),
|
|
29
|
+
tagList: z.array(ideaTagSchema).max(32).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type IdeaUpdateSchemaInput = z.infer<typeof ideaUpdateSchema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idea Link — browser-safe public barrel only.
|
|
3
|
+
*
|
|
4
|
+
* Node-only code lives in `./node/` and is exported from `src/node.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { BuildIdeaImageLinkDraftInput, BuildIdeaLinkDraftInput } from "./idea.build";
|
|
8
|
+
export { buildIdeaImageLinkDraft, buildIdeaLinkDraft } from "./idea.build";
|
|
9
|
+
export type { IdeaOriginal } from "./idea.interface";
|
|
10
|
+
export {
|
|
11
|
+
IDEA_IMAGE_LINK_NAME,
|
|
12
|
+
IDEA_IMAGE_TAG_RAW,
|
|
13
|
+
IDEA_IMAGE_TEMP_DIR,
|
|
14
|
+
IDEA_LINK_VALUE,
|
|
15
|
+
isIdeaImageLinkName,
|
|
16
|
+
} from "./idea.interface";
|
|
17
|
+
export { normalizeStringList } from "./idea.normalize";
|
|
18
|
+
export { parseIdeaOriginal } from "./idea.parse";
|
|
19
|
+
export { ideaLinkValueFilters } from "./idea.query";
|
|
20
|
+
export type { IdeaCreateSchemaInput, IdeaUpdateSchemaInput } from "./idea.schema";
|
|
21
|
+
export { ideaCreateSchema, ideaUpdateSchema } from "./idea.schema";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { IDEA_IMAGE_TEMP_DIR } from "../idea.interface";
|
|
2
|
+
|
|
3
|
+
/** Path suffix on upload-service public URL for temp file download. */
|
|
4
|
+
export const IDEA_TEMP_DOWNLOAD_PATH_SUFFIX = "/v0/upload/temp/download";
|
|
5
|
+
|
|
6
|
+
/** Flat path under temp sandbox: `idea/paste/{sha256}.{ext}`. */
|
|
7
|
+
export function buildIdeaPasteRelativePath(contentHash: string, ext: string): string {
|
|
8
|
+
const hash = contentHash.trim();
|
|
9
|
+
const extension = ext.trim().replace(/^\./, "");
|
|
10
|
+
if (hash === "" || extension === "") {
|
|
11
|
+
throw new Error("buildIdeaPasteRelativePath: contentHash and ext are required");
|
|
12
|
+
}
|
|
13
|
+
return `${IDEA_IMAGE_TEMP_DIR}/${hash}.${extension}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeRelativeSandboxPath(path: string): string | null {
|
|
17
|
+
const normalized = path.replace(/\\/g, "/").trim();
|
|
18
|
+
if (normalized === "" || normalized.startsWith("/") || normalized.includes("..")) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Build absolute locator stored in `details.original.url`. */
|
|
25
|
+
export function buildIdeaTempDownloadLocator(relativePath: string, publicBaseUrl: string): string {
|
|
26
|
+
const sanitized = normalizeRelativeSandboxPath(relativePath);
|
|
27
|
+
if (sanitized == null) {
|
|
28
|
+
throw new Error("buildIdeaTempDownloadLocator: invalid relativePath");
|
|
29
|
+
}
|
|
30
|
+
const base = publicBaseUrl.trim().replace(/\/+$/, "");
|
|
31
|
+
const url = new URL(`${base}${IDEA_TEMP_DOWNLOAD_PATH_SUFFIX}`);
|
|
32
|
+
url.searchParams.set("path", sanitized);
|
|
33
|
+
return url.href;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse temp download URL → sandbox-relative path, or null when not a temp download URL. */
|
|
37
|
+
export function parseIdeaTempDownloadLocator(locator: string): string | null {
|
|
38
|
+
const trimmed = locator.trim();
|
|
39
|
+
if (trimmed === "") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let parsed: URL;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(trimmed, "http://localhost");
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pathname = parsed.pathname.replace(/\\/g, "/");
|
|
51
|
+
if (!pathname.endsWith(IDEA_TEMP_DOWNLOAD_PATH_SUFFIX)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const pathParam = parsed.searchParams.get("path");
|
|
56
|
+
if (pathParam == null || pathParam.trim() === "") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return normalizeRelativeSandboxPath(pathParam.trim());
|
|
61
|
+
}
|
package/src/shared/log/README.md
CHANGED
|
@@ -18,8 +18,7 @@ src/shared/log/
|
|
|
18
18
|
|
|
19
19
|
upload/ # shared Supabase upload pipeline (Node)
|
|
20
20
|
env.ts # LOG_PERSIST_ENABLED, SERVICE, PROCESS, runtime dir
|
|
21
|
-
interface.ts # LogBatch meta,
|
|
22
|
-
batch-id.ts # computeLogBatchIdempotencyKey
|
|
21
|
+
interface.ts # LogBatch meta, LOG_BATCH_LIST_VALUE, lane type
|
|
23
22
|
flush.ts # buildLogListDraft, postLogBatch
|
|
24
23
|
hook.ts # LogManager → spool writer offer bridge
|
|
25
24
|
logger.ts # internal pipeline loggers (no feedback loop)
|
|
@@ -58,7 +57,7 @@ App code
|
|
|
58
57
|
→ {spoolRoot}/{serviceId}/{role}/*.tmp
|
|
59
58
|
|
|
60
59
|
Guard collect-log runner (1 min tick)
|
|
61
|
-
→ read *.tmp → postLogBatch → rename *.json
|
|
60
|
+
→ read *.tmp → postLogBatch (List value=log-batch, name=service) → rename *.json / .synced marker
|
|
62
61
|
→ GC old tmp/json (once per day; 7 d retention)
|
|
63
62
|
|
|
64
63
|
log-service (downstream)
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { List } from "../../../list/list.interface";
|
|
8
|
-
import type
|
|
9
|
-
import
|
|
8
|
+
import { LOG_BATCH_LIST_VALUE, type LogBatchMeta, type LogPersistLane } from "../upload/interface";
|
|
9
|
+
import type { LogEntry } from "./log-manager";
|
|
10
10
|
|
|
11
11
|
const LANES: ReadonlySet<string> = new Set<LogPersistLane>(["fast", "medium", "slow"]);
|
|
12
12
|
|
|
@@ -26,8 +26,8 @@ export interface FlatLogEntry extends LogEntry {
|
|
|
26
26
|
process: string;
|
|
27
27
|
lane: LogPersistLane;
|
|
28
28
|
pid: number;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/** Source List row id. */
|
|
30
|
+
listId: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -61,8 +61,7 @@ export function parseLogBatchMeta(rawMeta: unknown): LogBatchMeta | null {
|
|
|
61
61
|
const service = asString(source.service);
|
|
62
62
|
const processName = asString(source.process);
|
|
63
63
|
const laneRaw = asString(source.lane);
|
|
64
|
-
|
|
65
|
-
if (service == null || processName == null || laneRaw == null || !LANES.has(laneRaw) || idempotencyKey == null) {
|
|
64
|
+
if (service == null || processName == null || laneRaw == null || !LANES.has(laneRaw)) {
|
|
66
65
|
return null;
|
|
67
66
|
}
|
|
68
67
|
|
|
@@ -75,7 +74,6 @@ export function parseLogBatchMeta(rawMeta: unknown): LogBatchMeta | null {
|
|
|
75
74
|
from: asFiniteNumber(source.from),
|
|
76
75
|
to: asFiniteNumber(source.to),
|
|
77
76
|
flushedAt: asFiniteNumber(source.flushedAt) ?? 0,
|
|
78
|
-
idempotencyKey,
|
|
79
77
|
};
|
|
80
78
|
}
|
|
81
79
|
|
|
@@ -93,9 +91,9 @@ export function isLogEntry(value: unknown): value is LogEntry {
|
|
|
93
91
|
);
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
/** Is this List a persisted log batch (by
|
|
94
|
+
/** Is this List a persisted log batch (by `value`)? */
|
|
97
95
|
export function isLogBatchList(list: List): boolean {
|
|
98
|
-
return list.
|
|
96
|
+
return list.value === LOG_BATCH_LIST_VALUE;
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
/**
|
|
@@ -106,6 +104,10 @@ export function decodeLogBatch(list: List): DecodedLogBatch | null {
|
|
|
106
104
|
if (!isLogBatchList(list)) {
|
|
107
105
|
return null;
|
|
108
106
|
}
|
|
107
|
+
const loaderKey = list.details?.loaderKey;
|
|
108
|
+
if (typeof loaderKey === "string" && loaderKey.endsWith(".bad")) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
109
111
|
const meta = parseLogBatchMeta(list.details?.meta);
|
|
110
112
|
if (meta == null) {
|
|
111
113
|
return null;
|
|
@@ -142,7 +144,7 @@ export function flattenLogBatch(batch: DecodedLogBatch): FlatLogEntry[] {
|
|
|
142
144
|
process: batch.meta.process,
|
|
143
145
|
lane: batch.meta.lane,
|
|
144
146
|
pid: batch.meta.pid,
|
|
145
|
-
|
|
147
|
+
listId: batch.listId,
|
|
146
148
|
}));
|
|
147
149
|
}
|
|
148
150
|
|
package/src/shared/log/index.ts
CHANGED
|
@@ -42,5 +42,6 @@ export {
|
|
|
42
42
|
resolveLogData,
|
|
43
43
|
withModule,
|
|
44
44
|
} from "./core/log-scope";
|
|
45
|
+
export { formatLogSpoolBatchTimestamp } from "./spool/file";
|
|
45
46
|
export type { LogBatchMeta, LogPersistLane } from "./upload/interface";
|
|
46
|
-
export {
|
|
47
|
+
export { LOG_BATCH_LIST_VALUE } from "./upload/interface";
|