@baruchiro/paperless-mcp 1.0.0 → 2.0.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/README.md +288 -11
- package/build/api/PaperlessAPI.d.ts +29 -2
- package/build/api/PaperlessAPI.js +97 -8
- package/build/api/types.d.ts +47 -0
- package/build/index.js +28 -5
- package/build/resources/documents.d.ts +1 -2
- package/build/resources/documents.js +1 -36
- package/build/resources/documents.test.js +6 -18
- package/build/server.d.ts +14 -1
- package/build/server.js +17 -3
- package/build/server.test.d.ts +1 -0
- package/build/server.test.js +54 -0
- package/build/tools/documents.d.ts +2 -0
- package/build/tools/documents.js +147 -51
- package/build/tools/documents.test.js +404 -0
- package/build/tools/mail.d.ts +3 -0
- package/build/tools/mail.js +187 -0
- package/build/tools/mail.test.d.ts +1 -0
- package/build/tools/mail.test.js +212 -0
- package/build/tools/notes.d.ts +3 -0
- package/build/tools/notes.js +55 -0
- package/build/tools/notes.test.d.ts +1 -0
- package/build/tools/notes.test.js +177 -0
- package/build/tools/utils/descriptions.d.ts +1 -1
- package/build/tools/utils/descriptions.js +1 -1
- package/build/tools/utils/documentQuery.d.ts +71 -0
- package/build/tools/utils/documentQuery.js +270 -0
- package/build/tools/utils/selectFields.d.ts +11 -0
- package/build/tools/utils/selectFields.js +91 -0
- package/build/tools/utils/selectFields.test.d.ts +1 -0
- package/build/tools/utils/selectFields.test.js +164 -0
- package/package.json +1 -1
- package/paperless-mcp.dxt +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DOCUMENT_QUERY_PAPERLESS_FILTER_KEYS = exports.SEARCH_DOCUMENTS_ARGS_SHAPE = exports.QUERY_DOCUMENTS_ARGS_SHAPE = exports.LIST_DOCUMENTS_ARGS_SHAPE = exports.paperlessFiltersSchema = exports.paperlessFilterValueSchema = exports.customFieldQuerySchema = void 0;
|
|
4
|
+
exports.buildDocumentQueryString = buildDocumentQueryString;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const CUSTOM_FIELD_QUERY_GROUP_OPERATORS = ["AND", "OR"];
|
|
7
|
+
const customFieldQueryPrimitiveSchema = zod_1.z.union([
|
|
8
|
+
zod_1.z.string(),
|
|
9
|
+
zod_1.z.number(),
|
|
10
|
+
zod_1.z.boolean(),
|
|
11
|
+
zod_1.z.null(),
|
|
12
|
+
]);
|
|
13
|
+
const customFieldQueryValueSchema = zod_1.z.union([
|
|
14
|
+
customFieldQueryPrimitiveSchema,
|
|
15
|
+
zod_1.z.array(customFieldQueryPrimitiveSchema),
|
|
16
|
+
]);
|
|
17
|
+
function isCustomFieldQueryPrimitive(value) {
|
|
18
|
+
return (typeof value === "string" ||
|
|
19
|
+
typeof value === "number" ||
|
|
20
|
+
typeof value === "boolean" ||
|
|
21
|
+
value === null);
|
|
22
|
+
}
|
|
23
|
+
function isCustomFieldQueryValue(value) {
|
|
24
|
+
if (isCustomFieldQueryPrimitive(value)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return (Array.isArray(value) && value.every((item) => isCustomFieldQueryPrimitive(item)));
|
|
28
|
+
}
|
|
29
|
+
function isCustomFieldQueryGroupOperator(value) {
|
|
30
|
+
return (typeof value === "string" &&
|
|
31
|
+
CUSTOM_FIELD_QUERY_GROUP_OPERATORS.includes(value));
|
|
32
|
+
}
|
|
33
|
+
function isCustomFieldQuery(value) {
|
|
34
|
+
if (!Array.isArray(value)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (value.length === 3 &&
|
|
38
|
+
(typeof value[0] === "string" || typeof value[0] === "number") &&
|
|
39
|
+
!isCustomFieldQueryGroupOperator(value[0]) &&
|
|
40
|
+
typeof value[1] === "string" &&
|
|
41
|
+
isCustomFieldQueryValue(value[2])) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (value.length === 2 &&
|
|
45
|
+
isCustomFieldQueryGroupOperator(value[0]) &&
|
|
46
|
+
Array.isArray(value[1]) &&
|
|
47
|
+
value[1].length >= 1) {
|
|
48
|
+
return value[1].every((item) => isCustomFieldQuery(item));
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
exports.customFieldQuerySchema = zod_1.z
|
|
53
|
+
.array(zod_1.z.unknown())
|
|
54
|
+
.superRefine((value, ctx) => {
|
|
55
|
+
if (!isCustomFieldQuery(value)) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
58
|
+
message: "Invalid custom_field_query. Use [field_name_or_id, operator, value] or ['AND'|'OR', [clause1, clause2]].",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const paperlessFilterScalarSchema = zod_1.z.union([
|
|
63
|
+
zod_1.z.string(),
|
|
64
|
+
zod_1.z.number(),
|
|
65
|
+
zod_1.z.boolean(),
|
|
66
|
+
]);
|
|
67
|
+
exports.paperlessFilterValueSchema = zod_1.z.union([
|
|
68
|
+
paperlessFilterScalarSchema,
|
|
69
|
+
zod_1.z.array(paperlessFilterScalarSchema),
|
|
70
|
+
]);
|
|
71
|
+
exports.paperlessFiltersSchema = zod_1.z.record(exports.paperlessFilterValueSchema);
|
|
72
|
+
const DOCUMENT_QUERY_BASE_ARGS_SHAPE = {
|
|
73
|
+
page: zod_1.z.number().optional(),
|
|
74
|
+
page_size: zod_1.z.number().optional(),
|
|
75
|
+
search: zod_1.z.string().optional(),
|
|
76
|
+
correspondent: zod_1.z.number().optional(),
|
|
77
|
+
document_type: zod_1.z.number().optional(),
|
|
78
|
+
tag: zod_1.z.number().optional(),
|
|
79
|
+
storage_path: zod_1.z.number().optional(),
|
|
80
|
+
created__date__gte: zod_1.z.string().optional(),
|
|
81
|
+
created__date__lte: zod_1.z.string().optional(),
|
|
82
|
+
ordering: zod_1.z.string().optional(),
|
|
83
|
+
archive_serial_number: zod_1.z.number().optional(),
|
|
84
|
+
archive_serial_number__isnull: zod_1.z.boolean().optional(),
|
|
85
|
+
custom_fields__icontains: zod_1.z.string().min(1).optional(),
|
|
86
|
+
};
|
|
87
|
+
exports.LIST_DOCUMENTS_ARGS_SHAPE = Object.assign(Object.assign({}, DOCUMENT_QUERY_BASE_ARGS_SHAPE), { custom_field_query: zod_1.z.string().min(1).optional() });
|
|
88
|
+
exports.QUERY_DOCUMENTS_ARGS_SHAPE = Object.assign(Object.assign({}, DOCUMENT_QUERY_BASE_ARGS_SHAPE), { query: zod_1.z.string().optional(), more_like_id: zod_1.z.number().optional(), custom_field_query: exports.customFieldQuerySchema
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Paperless custom field query. Use [field_name_or_id, operator, value] for a single clause or ['AND'|'OR', [clause1, clause2]] for grouped clauses."), paperless_filters: exports.paperlessFiltersSchema
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Additional documented /api/documents/ Paperless filters. Keys must match Paperless query parameter names exactly. Prefer first-class arguments when available.") });
|
|
93
|
+
exports.SEARCH_DOCUMENTS_ARGS_SHAPE = {
|
|
94
|
+
query: zod_1.z.string(),
|
|
95
|
+
};
|
|
96
|
+
const FIRST_CLASS_QUERY_PARAM_MAP = {
|
|
97
|
+
page: "page",
|
|
98
|
+
page_size: "page_size",
|
|
99
|
+
ordering: "ordering",
|
|
100
|
+
query: "query",
|
|
101
|
+
search: "search",
|
|
102
|
+
more_like_id: "more_like_id",
|
|
103
|
+
correspondent: "correspondent__id",
|
|
104
|
+
document_type: "document_type__id",
|
|
105
|
+
tag: "tags__id",
|
|
106
|
+
storage_path: "storage_path__id",
|
|
107
|
+
created__date__gte: "created__date__gte",
|
|
108
|
+
created__date__lte: "created__date__lte",
|
|
109
|
+
archive_serial_number: "archive_serial_number",
|
|
110
|
+
archive_serial_number__isnull: "archive_serial_number__isnull",
|
|
111
|
+
custom_fields__icontains: "custom_fields__icontains",
|
|
112
|
+
};
|
|
113
|
+
// Derived from the documented /api/documents/ query parameters in Paperless_ngx_REST_API.yaml.
|
|
114
|
+
exports.DOCUMENT_QUERY_PAPERLESS_FILTER_KEYS = [
|
|
115
|
+
"added__date__gt",
|
|
116
|
+
"added__date__gte",
|
|
117
|
+
"added__date__lt",
|
|
118
|
+
"added__date__lte",
|
|
119
|
+
"added__day",
|
|
120
|
+
"added__gt",
|
|
121
|
+
"added__gte",
|
|
122
|
+
"added__lt",
|
|
123
|
+
"added__lte",
|
|
124
|
+
"added__month",
|
|
125
|
+
"added__year",
|
|
126
|
+
"archive_serial_number",
|
|
127
|
+
"archive_serial_number__gt",
|
|
128
|
+
"archive_serial_number__gte",
|
|
129
|
+
"archive_serial_number__isnull",
|
|
130
|
+
"archive_serial_number__lt",
|
|
131
|
+
"archive_serial_number__lte",
|
|
132
|
+
"checksum__icontains",
|
|
133
|
+
"checksum__iendswith",
|
|
134
|
+
"checksum__iexact",
|
|
135
|
+
"checksum__istartswith",
|
|
136
|
+
"content__icontains",
|
|
137
|
+
"content__iendswith",
|
|
138
|
+
"content__iexact",
|
|
139
|
+
"content__istartswith",
|
|
140
|
+
"correspondent__id",
|
|
141
|
+
"correspondent__id__in",
|
|
142
|
+
"correspondent__id__none",
|
|
143
|
+
"correspondent__isnull",
|
|
144
|
+
"correspondent__name__icontains",
|
|
145
|
+
"correspondent__name__iendswith",
|
|
146
|
+
"correspondent__name__iexact",
|
|
147
|
+
"correspondent__name__istartswith",
|
|
148
|
+
"created__date__gt",
|
|
149
|
+
"created__date__gte",
|
|
150
|
+
"created__date__lt",
|
|
151
|
+
"created__date__lte",
|
|
152
|
+
"created__day",
|
|
153
|
+
"created__gt",
|
|
154
|
+
"created__gte",
|
|
155
|
+
"created__lt",
|
|
156
|
+
"created__lte",
|
|
157
|
+
"created__month",
|
|
158
|
+
"created__year",
|
|
159
|
+
"custom_field_query",
|
|
160
|
+
"custom_fields__icontains",
|
|
161
|
+
"custom_fields__id__all",
|
|
162
|
+
"custom_fields__id__in",
|
|
163
|
+
"custom_fields__id__none",
|
|
164
|
+
"document_type__id",
|
|
165
|
+
"document_type__id__in",
|
|
166
|
+
"document_type__id__none",
|
|
167
|
+
"document_type__isnull",
|
|
168
|
+
"document_type__name__icontains",
|
|
169
|
+
"document_type__name__iendswith",
|
|
170
|
+
"document_type__name__iexact",
|
|
171
|
+
"document_type__name__istartswith",
|
|
172
|
+
"fields",
|
|
173
|
+
"full_perms",
|
|
174
|
+
"has_custom_fields",
|
|
175
|
+
"id",
|
|
176
|
+
"id__in",
|
|
177
|
+
"is_in_inbox",
|
|
178
|
+
"is_tagged",
|
|
179
|
+
"mime_type",
|
|
180
|
+
"modified__date__gt",
|
|
181
|
+
"modified__date__gte",
|
|
182
|
+
"modified__date__lt",
|
|
183
|
+
"modified__date__lte",
|
|
184
|
+
"modified__day",
|
|
185
|
+
"modified__gt",
|
|
186
|
+
"modified__gte",
|
|
187
|
+
"modified__lt",
|
|
188
|
+
"modified__lte",
|
|
189
|
+
"modified__month",
|
|
190
|
+
"modified__year",
|
|
191
|
+
"ordering",
|
|
192
|
+
"original_filename__icontains",
|
|
193
|
+
"original_filename__iendswith",
|
|
194
|
+
"original_filename__iexact",
|
|
195
|
+
"original_filename__istartswith",
|
|
196
|
+
"owner__id",
|
|
197
|
+
"owner__id__in",
|
|
198
|
+
"owner__id__none",
|
|
199
|
+
"owner__isnull",
|
|
200
|
+
"page",
|
|
201
|
+
"page_size",
|
|
202
|
+
"query",
|
|
203
|
+
"search",
|
|
204
|
+
"shared_by__id",
|
|
205
|
+
"storage_path__id",
|
|
206
|
+
"storage_path__id__in",
|
|
207
|
+
"storage_path__id__none",
|
|
208
|
+
"storage_path__isnull",
|
|
209
|
+
"storage_path__name__icontains",
|
|
210
|
+
"storage_path__name__iendswith",
|
|
211
|
+
"storage_path__name__iexact",
|
|
212
|
+
"storage_path__name__istartswith",
|
|
213
|
+
"tags__id",
|
|
214
|
+
"tags__id__all",
|
|
215
|
+
"tags__id__in",
|
|
216
|
+
"tags__id__none",
|
|
217
|
+
"tags__name__icontains",
|
|
218
|
+
"tags__name__iendswith",
|
|
219
|
+
"tags__name__iexact",
|
|
220
|
+
"tags__name__istartswith",
|
|
221
|
+
"title__icontains",
|
|
222
|
+
"title__iendswith",
|
|
223
|
+
"title__iexact",
|
|
224
|
+
"title__istartswith",
|
|
225
|
+
"title_content",
|
|
226
|
+
];
|
|
227
|
+
const DOCUMENT_QUERY_PAPERLESS_FILTER_KEY_SET = new Set(exports.DOCUMENT_QUERY_PAPERLESS_FILTER_KEYS);
|
|
228
|
+
function hasValue(value) {
|
|
229
|
+
return value !== undefined && value !== null;
|
|
230
|
+
}
|
|
231
|
+
function setQueryParam(query, key, value, jsonEncode = false) {
|
|
232
|
+
if (jsonEncode) {
|
|
233
|
+
query.set(key, JSON.stringify(value));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (Array.isArray(value)) {
|
|
237
|
+
query.set(key, value.map((item) => String(item)).join(","));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
query.set(key, String(value));
|
|
241
|
+
}
|
|
242
|
+
function buildDocumentQueryString(args) {
|
|
243
|
+
const query = new URLSearchParams();
|
|
244
|
+
const firstClassKeys = new Set();
|
|
245
|
+
for (const [argName, queryParamName] of Object.entries(FIRST_CLASS_QUERY_PARAM_MAP)) {
|
|
246
|
+
const value = args[argName];
|
|
247
|
+
if (!hasValue(value)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
setQueryParam(query, queryParamName, value);
|
|
251
|
+
firstClassKeys.add(queryParamName);
|
|
252
|
+
}
|
|
253
|
+
if (hasValue(args.custom_field_query)) {
|
|
254
|
+
setQueryParam(query, "custom_field_query", args.custom_field_query, typeof args.custom_field_query !== "string");
|
|
255
|
+
firstClassKeys.add("custom_field_query");
|
|
256
|
+
}
|
|
257
|
+
if (!args.paperless_filters) {
|
|
258
|
+
return query.toString() ? `?${query.toString()}` : "";
|
|
259
|
+
}
|
|
260
|
+
for (const [key, value] of Object.entries(args.paperless_filters)) {
|
|
261
|
+
if (!DOCUMENT_QUERY_PAPERLESS_FILTER_KEY_SET.has(key)) {
|
|
262
|
+
throw new Error(`Unsupported paperless_filters key '${key}'. Use documented /api/documents/ query parameter names only.`);
|
|
263
|
+
}
|
|
264
|
+
if (firstClassKeys.has(key)) {
|
|
265
|
+
throw new Error(`Duplicate filter '${key}' provided both as a first-class argument and in paperless_filters.`);
|
|
266
|
+
}
|
|
267
|
+
setQueryParam(query, key, value);
|
|
268
|
+
}
|
|
269
|
+
return query.toString() ? `?${query.toString()}` : "";
|
|
270
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PaperlessAPI } from "../../api/PaperlessAPI";
|
|
2
|
+
import { CustomField, CustomFieldInstanceRequest, CustomFieldValue } from "../../api/types";
|
|
3
|
+
/**
|
|
4
|
+
* Encoding Paperless expects for a select value: `update_document` takes the
|
|
5
|
+
* option index; `bulk_edit` writes `value_select` directly so it needs the
|
|
6
|
+
* stored form (option id on 2.17+, index on pre-2.17 string options).
|
|
7
|
+
*/
|
|
8
|
+
export type SelectValueEncoding = "index" | "stored";
|
|
9
|
+
/** Translates a select value (label, option id, or index) to the `encoding` Paperless expects; throws on no match. */
|
|
10
|
+
export declare function resolveSelectCustomFieldValue(field: CustomField, value: CustomFieldValue, encoding?: SelectValueEncoding): CustomFieldValue;
|
|
11
|
+
export declare function resolveSelectCustomFieldValues(api: PaperlessAPI, customFields: CustomFieldInstanceRequest[] | undefined, encoding?: SelectValueEncoding): Promise<CustomFieldInstanceRequest[] | undefined>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.resolveSelectCustomFieldValue = resolveSelectCustomFieldValue;
|
|
13
|
+
exports.resolveSelectCustomFieldValues = resolveSelectCustomFieldValues;
|
|
14
|
+
function normalizeSelectOptions(field) {
|
|
15
|
+
var _a;
|
|
16
|
+
const rawOptions = (_a = field.extra_data) === null || _a === void 0 ? void 0 : _a.select_options;
|
|
17
|
+
if (!Array.isArray(rawOptions)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
return rawOptions.map((option, index) => {
|
|
21
|
+
if (option && typeof option === "object") {
|
|
22
|
+
const { id, label } = option;
|
|
23
|
+
return {
|
|
24
|
+
index,
|
|
25
|
+
label: typeof label === "string" ? label : String(label),
|
|
26
|
+
id: typeof id === "string" ? id : undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { index, label: String(option) };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function findSelectOption(options, value) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
const byLabel = options.find((option) => option.label === value);
|
|
35
|
+
const byId = options.find((option) => option.id === value);
|
|
36
|
+
if (byLabel && byId && byLabel.index !== byId.index) {
|
|
37
|
+
throw new Error(`Ambiguous select value ${JSON.stringify(value)}: it matches one ` +
|
|
38
|
+
`option's label and a different option's id.`);
|
|
39
|
+
}
|
|
40
|
+
return byLabel !== null && byLabel !== void 0 ? byLabel : byId;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
43
|
+
return options.find((option) => option.index === value);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Translates a select value (label, option id, or index) to the `encoding` Paperless expects; throws on no match. */
|
|
48
|
+
function resolveSelectCustomFieldValue(field, value, encoding = "index") {
|
|
49
|
+
var _a;
|
|
50
|
+
if (field.data_type !== "select" || value === null) {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
const options = normalizeSelectOptions(field);
|
|
54
|
+
if (options.length === 0) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
const option = findSelectOption(options, value);
|
|
58
|
+
if (!option) {
|
|
59
|
+
const optionList = options
|
|
60
|
+
.map((o) => `${JSON.stringify(o.label)} (index ${o.index})`)
|
|
61
|
+
.join(", ");
|
|
62
|
+
throw new Error(`Invalid value ${JSON.stringify(value)} for select custom field ` +
|
|
63
|
+
`"${field.name}" (id ${field.id}). Pass one of the option labels. ` +
|
|
64
|
+
`Valid options: ${optionList}.`);
|
|
65
|
+
}
|
|
66
|
+
return encoding === "stored" ? (_a = option.id) !== null && _a !== void 0 ? _a : option.index : option.index;
|
|
67
|
+
}
|
|
68
|
+
function resolveSelectCustomFieldValues(api_1, customFields_1) {
|
|
69
|
+
return __awaiter(this, arguments, void 0, function* (api, customFields, encoding = "index") {
|
|
70
|
+
if (!customFields || customFields.length === 0) {
|
|
71
|
+
return customFields;
|
|
72
|
+
}
|
|
73
|
+
const uniqueFieldIds = [...new Set(customFields.map((cf) => cf.field))];
|
|
74
|
+
const definitions = new Map();
|
|
75
|
+
yield Promise.all(uniqueFieldIds.map((id) => __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
try {
|
|
77
|
+
definitions.set(id, yield api.getCustomField(id));
|
|
78
|
+
}
|
|
79
|
+
catch (_a) {
|
|
80
|
+
// Definition unavailable; leave the value for Paperless to validate.
|
|
81
|
+
}
|
|
82
|
+
})));
|
|
83
|
+
return customFields.map((cf) => {
|
|
84
|
+
const field = definitions.get(cf.field);
|
|
85
|
+
if (!field || field.data_type !== "select") {
|
|
86
|
+
return cf;
|
|
87
|
+
}
|
|
88
|
+
return Object.assign(Object.assign({}, cf), { value: resolveSelectCustomFieldValue(field, cf.value, encoding) });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const node_test_1 = require("node:test");
|
|
16
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
17
|
+
const selectFields_1 = require("./selectFields");
|
|
18
|
+
const LEGACY_SELECT_FIELD = {
|
|
19
|
+
id: 2,
|
|
20
|
+
name: "Retention period",
|
|
21
|
+
data_type: "select",
|
|
22
|
+
extra_data: { select_options: ["1 year", "7 years", "2 years"], default_currency: null },
|
|
23
|
+
document_count: 10,
|
|
24
|
+
};
|
|
25
|
+
const OBJECT_SELECT_FIELD = {
|
|
26
|
+
id: 3,
|
|
27
|
+
name: "Priority",
|
|
28
|
+
data_type: "select",
|
|
29
|
+
extra_data: {
|
|
30
|
+
select_options: [
|
|
31
|
+
{ id: "abc123", label: "Low" },
|
|
32
|
+
{ id: "def456", label: "High" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
document_count: 5,
|
|
36
|
+
};
|
|
37
|
+
const STRING_FIELD = {
|
|
38
|
+
id: 4,
|
|
39
|
+
name: "Reference",
|
|
40
|
+
data_type: "string",
|
|
41
|
+
document_count: 1,
|
|
42
|
+
};
|
|
43
|
+
(0, node_test_1.describe)("resolveSelectCustomFieldValue", () => {
|
|
44
|
+
(0, node_test_1.test)("translates a label to its zero-based index (pre-2.17 string options)", () => {
|
|
45
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, "1 year"), 0);
|
|
46
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, "7 years"), 1);
|
|
47
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, "2 years"), 2);
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.test)("translates a label to its zero-based index (2.17+ object options)", () => {
|
|
50
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "Low"), 0);
|
|
51
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "High"), 1);
|
|
52
|
+
});
|
|
53
|
+
(0, node_test_1.test)("passes through an already-encoded index (pre-2.17)", () => {
|
|
54
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, 1), 1);
|
|
55
|
+
});
|
|
56
|
+
(0, node_test_1.test)("maps an option id back to its zero-based index (2.17+ round-trip)", () => {
|
|
57
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "def456"), 1);
|
|
58
|
+
});
|
|
59
|
+
(0, node_test_1.test)("resolves both the label and the option id to the same index", () => {
|
|
60
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "Low"), 0);
|
|
61
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "abc123"), 0);
|
|
62
|
+
});
|
|
63
|
+
(0, node_test_1.test)("returns null unchanged so the field can be cleared", () => {
|
|
64
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, null), null);
|
|
65
|
+
});
|
|
66
|
+
(0, node_test_1.test)("leaves non-select field values untouched", () => {
|
|
67
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(STRING_FIELD, "1 year"), "1 year");
|
|
68
|
+
});
|
|
69
|
+
(0, node_test_1.test)("throws an actionable error listing valid options for an unknown value", () => {
|
|
70
|
+
strict_1.default.throws(() => (0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, "forever"), (err) => {
|
|
71
|
+
strict_1.default.match(err.message, /forever/);
|
|
72
|
+
strict_1.default.match(err.message, /Retention period/);
|
|
73
|
+
strict_1.default.match(err.message, /1 year/);
|
|
74
|
+
strict_1.default.match(err.message, /7 years/);
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
(0, node_test_1.test)("rejects an out-of-range index rather than forwarding it", () => {
|
|
79
|
+
strict_1.default.throws(() => (0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, 9));
|
|
80
|
+
});
|
|
81
|
+
(0, node_test_1.test)("throws on a value that matches one option's label and another's id", () => {
|
|
82
|
+
const collidingField = {
|
|
83
|
+
id: 5,
|
|
84
|
+
name: "Collision",
|
|
85
|
+
data_type: "select",
|
|
86
|
+
extra_data: {
|
|
87
|
+
select_options: [
|
|
88
|
+
{ id: "High", label: "Low" },
|
|
89
|
+
{ id: "xyz789", label: "High" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
document_count: 0,
|
|
93
|
+
};
|
|
94
|
+
strict_1.default.throws(() => (0, selectFields_1.resolveSelectCustomFieldValue)(collidingField, "High"), /Ambiguous/);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
(0, node_test_1.describe)("resolveSelectCustomFieldValue with stored encoding (bulk_edit path)", () => {
|
|
98
|
+
(0, node_test_1.test)("translates a label to the option id on 2.17+ object options", () => {
|
|
99
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "Low", "stored"), "abc123");
|
|
100
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "High", "stored"), "def456");
|
|
101
|
+
});
|
|
102
|
+
(0, node_test_1.test)("translates a label to the index on pre-2.17 string options (no id to store)", () => {
|
|
103
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(LEGACY_SELECT_FIELD, "7 years", "stored"), 1);
|
|
104
|
+
});
|
|
105
|
+
(0, node_test_1.test)("passes an already-stored option id through unchanged", () => {
|
|
106
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, "def456", "stored"), "def456");
|
|
107
|
+
});
|
|
108
|
+
(0, node_test_1.test)("maps an option index to its stored id (2.17+)", () => {
|
|
109
|
+
strict_1.default.equal((0, selectFields_1.resolveSelectCustomFieldValue)(OBJECT_SELECT_FIELD, 1, "stored"), "def456");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
function apiReturning(fields) {
|
|
113
|
+
const requestedIds = [];
|
|
114
|
+
const fieldMap = new Map(fields.map((field) => [field.id, field]));
|
|
115
|
+
const api = {
|
|
116
|
+
getCustomField: (id) => __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
requestedIds.push(id);
|
|
118
|
+
const field = fieldMap.get(id);
|
|
119
|
+
if (!field)
|
|
120
|
+
throw new Error(`custom field ${id} not found`);
|
|
121
|
+
return field;
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
return { api, requestedIds };
|
|
125
|
+
}
|
|
126
|
+
(0, node_test_1.describe)("resolveSelectCustomFieldValues", () => {
|
|
127
|
+
(0, node_test_1.test)("returns undefined/empty input unchanged without fetching definitions", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
const { api, requestedIds } = apiReturning([]);
|
|
129
|
+
strict_1.default.equal(yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, undefined), undefined);
|
|
130
|
+
strict_1.default.deepEqual(yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, []), []);
|
|
131
|
+
strict_1.default.deepEqual(requestedIds, []);
|
|
132
|
+
}));
|
|
133
|
+
(0, node_test_1.test)("resolves select labels and leaves other fields untouched", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
const { api } = apiReturning([LEGACY_SELECT_FIELD, STRING_FIELD]);
|
|
135
|
+
const resolved = yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, [
|
|
136
|
+
{ field: 2, value: "2 years" },
|
|
137
|
+
{ field: 4, value: "INV-001" },
|
|
138
|
+
]);
|
|
139
|
+
strict_1.default.deepEqual(resolved, [
|
|
140
|
+
{ field: 2, value: 2 },
|
|
141
|
+
{ field: 4, value: "INV-001" },
|
|
142
|
+
]);
|
|
143
|
+
}));
|
|
144
|
+
(0, node_test_1.test)("applies the stored encoding (option id) when requested", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
|
+
const { api } = apiReturning([OBJECT_SELECT_FIELD]);
|
|
146
|
+
const resolved = yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, [{ field: 3, value: "High" }], "stored");
|
|
147
|
+
strict_1.default.deepEqual(resolved, [{ field: 3, value: "def456" }]);
|
|
148
|
+
}));
|
|
149
|
+
(0, node_test_1.test)("fetches each referenced field definition only once", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
const { api, requestedIds } = apiReturning([LEGACY_SELECT_FIELD]);
|
|
151
|
+
yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, [
|
|
152
|
+
{ field: 2, value: "1 year" },
|
|
153
|
+
{ field: 2, value: "7 years" },
|
|
154
|
+
]);
|
|
155
|
+
strict_1.default.deepEqual(requestedIds, [2]);
|
|
156
|
+
}));
|
|
157
|
+
(0, node_test_1.test)("passes the value through when the field definition cannot be fetched", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
|
+
const { api } = apiReturning([]);
|
|
159
|
+
const resolved = yield (0, selectFields_1.resolveSelectCustomFieldValues)(api, [
|
|
160
|
+
{ field: 99, value: "whatever" },
|
|
161
|
+
]);
|
|
162
|
+
strict_1.default.deepEqual(resolved, [{ field: 99, value: "whatever" }]);
|
|
163
|
+
}));
|
|
164
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baruchiro/paperless-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for interacting with Paperless-NGX document management system. Enables AI assistants to manage documents, tags, correspondents, and document types through the Paperless-NGX API.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"bin": {
|
package/paperless-mcp.dxt
CHANGED
|
Binary file
|