target-supabase-sdk 0.2.22 → 0.2.24
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 +2 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/chunks/{task-queue-DWFe0rCS.js → task-queue-C_nUaM2S.js} +260 -37
- package/dist/chunks/task-queue-C_nUaM2S.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 +113 -6
- package/dist/node.js.map +1 -1
- 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/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/task/task-node.d.ts +3 -0
- package/dist/task/task-node.d.ts.map +1 -1
- package/dist/task/task.api.d.ts +14 -1
- package/dist/task/task.api.d.ts.map +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 +21 -0
- 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/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/task/task-node.ts +7 -1
- package/src/task/task.api.ts +62 -15
- package/src/trigger/trigger.api.ts +6 -1
- package/dist/chunks/task-queue-DWFe0rCS.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/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/task/task.utils.ts +0 -0
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import type { QueryFilter } from "../../core.interface";
|
|
2
|
-
import {
|
|
2
|
+
import { linkDedupFilters } from "../../link/link.query";
|
|
3
3
|
import type { MediaValue } from "./media.interface";
|
|
4
4
|
|
|
5
5
|
/** Dedup key: `category=link` + `value` + `name` (matches register / find). */
|
|
6
6
|
export function mediaLinkDedupFilters(input: { value: MediaValue; name: string }): QueryFilter[] {
|
|
7
|
-
|
|
8
|
-
return [
|
|
9
|
-
{ field: "category", operator: "eq", value: CategoryLink.LINK },
|
|
10
|
-
{ field: "value", operator: "eq", value: input.value },
|
|
11
|
-
{ field: "name", operator: "eq", value: name },
|
|
12
|
-
];
|
|
7
|
+
return linkDedupFilters(input);
|
|
13
8
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { QueryFilter } from "../../core.interface";
|
|
2
|
-
import {
|
|
2
|
+
import { linkDedupFilters } from "../../link/link.query";
|
|
3
3
|
import { REVIEW_VALUE } from "./review.interface";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -7,10 +7,5 @@ import { REVIEW_VALUE } from "./review.interface";
|
|
|
7
7
|
* Many rows may match — the SDK does not define dedup for reviews.
|
|
8
8
|
*/
|
|
9
9
|
export function reviewLinksForNameFilters(name: string): QueryFilter[] {
|
|
10
|
-
|
|
11
|
-
return [
|
|
12
|
-
{ field: "category", operator: "eq", value: CategoryLink.LINK },
|
|
13
|
-
{ field: "value", operator: "eq", value: REVIEW_VALUE },
|
|
14
|
-
{ field: "name", operator: "eq", value: trimmed },
|
|
15
|
-
];
|
|
10
|
+
return linkDedupFilters({ value: REVIEW_VALUE, name });
|
|
16
11
|
}
|
|
@@ -15,7 +15,7 @@ export function getConfigFileDir(configFilePath: string): string {
|
|
|
15
15
|
* - Absolute `pathInConfig` → returned as-is
|
|
16
16
|
* - Relative `pathInConfig` → `resolve(dirname(configFilePath), pathInConfig)`
|
|
17
17
|
*
|
|
18
|
-
* **Not** relative to `process.cwd()`. See `.cursor/skills/
|
|
18
|
+
* **Not** relative to `process.cwd()`. See `.cursor/skills/task-local-discovery/SKILL.md`.
|
|
19
19
|
*/
|
|
20
20
|
export function resolvePathFromConfigFile(configFilePath: string, pathInConfig: string): string {
|
|
21
21
|
const trimmed = pathInConfig.trim();
|
package/src/task/index.ts
CHANGED
|
@@ -244,7 +244,7 @@ async function runBootstrapScan(options: BootstrapLocalTasksOptions): Promise<Bo
|
|
|
244
244
|
* ./tasks/my-task/index.mjs
|
|
245
245
|
* ```
|
|
246
246
|
*
|
|
247
|
-
* `taskDir` / `entry` paths are relative to **their config file's directory** (see
|
|
247
|
+
* `taskDir` / `entry` paths are relative to **their config file's directory** (see task-local-discovery skill).
|
|
248
248
|
*
|
|
249
249
|
* Legacy fallback: `./task.config.js` at project root (`taskDir: "./tasks"`).
|
|
250
250
|
*/
|
package/src/task/task-node.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { isOptimisticLockResponse } from "../core.api";
|
|
|
2
2
|
import { BaseNodeRuntime, LOG_TOPIC_NODE, type NodeLoopContext } from "../node/node-runtime.base";
|
|
3
3
|
import { createLogger, createScope, type LoggerWithScope, withModule } from "../shared/log";
|
|
4
4
|
import { getErrorMessage, toError } from "../shared/utils/error.utils";
|
|
5
|
-
import { patchChangeTaskStatus, patchClaimTask } from "./task.api";
|
|
5
|
+
import { type PatchClaimTaskPayload, patchChangeTaskStatus, patchClaimTask } from "./task.api";
|
|
6
6
|
import { CRITICAL_UNKNOWN_TASK_TRACE_ID, LOG_TOPIC_TASK } from "./task.constant";
|
|
7
7
|
import { TaskStatusAction } from "./task.interface";
|
|
8
8
|
import { TaskManager, type TaskRunResult } from "./task-manager";
|
|
@@ -19,6 +19,11 @@ class TaskNode extends BaseNodeRuntime {
|
|
|
19
19
|
return [...this._availableTaskList];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Optional claim filters passed to {@link patchClaimTask} (e.g. name prefix per task type). */
|
|
23
|
+
protected getPatchClaimTaskOptions(): Pick<PatchClaimTaskPayload, "namePrefixList"> {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
constructor() {
|
|
23
28
|
super("taskNode");
|
|
24
29
|
}
|
|
@@ -63,6 +68,7 @@ class TaskNode extends BaseNodeRuntime {
|
|
|
63
68
|
nodeId,
|
|
64
69
|
availableTaskList: [...availableTaskList],
|
|
65
70
|
traceId: loopTraceId,
|
|
71
|
+
...this.getPatchClaimTaskOptions(),
|
|
66
72
|
});
|
|
67
73
|
if (claimError) {
|
|
68
74
|
if (isOptimisticLockResponse(claimError)) {
|