@volter/twin-upstashvector 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +233 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +45 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +106 -0
- package/dist/src/upstashvector-budget.d.ts +84 -0
- package/dist/src/upstashvector-budget.js +443 -0
- package/dist/src/upstashvector-capabilities.d.ts +4 -0
- package/dist/src/upstashvector-capabilities.js +1108 -0
- package/dist/src/upstashvector-conformance.d.ts +7 -0
- package/dist/src/upstashvector-conformance.js +163 -0
- package/dist/src/upstashvector-connector.d.ts +109 -0
- package/dist/src/upstashvector-connector.js +286 -0
- package/dist/src/upstashvector-filter.d.ts +80 -0
- package/dist/src/upstashvector-filter.js +564 -0
- package/dist/src/upstashvector-server.d.ts +32 -0
- package/dist/src/upstashvector-server.js +56 -0
- package/dist/src/upstashvector-store.d.ts +248 -0
- package/dist/src/upstashvector-store.js +883 -0
- package/dist/src/upstashvector-twin.d.ts +67 -0
- package/dist/src/upstashvector-twin.js +287 -0
- package/package.json +51 -0
- package/src/cli.ts +47 -0
- package/src/index.ts +193 -0
- package/src/upstashvector-budget.ts +489 -0
- package/src/upstashvector-capabilities.ts +1242 -0
- package/src/upstashvector-conformance.ts +175 -0
- package/src/upstashvector-connector.ts +328 -0
- package/src/upstashvector-filter.ts +525 -0
- package/src/upstashvector-server.ts +86 -0
- package/src/upstashvector-store.ts +944 -0
- package/src/upstashvector-twin.ts +347 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// UPSTASH VECTOR REST TWIN — THE REQUEST HANDLER. Contract:
|
|
2
|
+
// handleUpstashVectorTwinRequest({ method, path, body, headers, root, occurredAt, readOnly, token })
|
|
3
|
+
// -> { status, body, headers }
|
|
4
|
+
//
|
|
5
|
+
// This file is the TRANSPORT half; every vector semantic lives in `upstashvector-store.ts` and the
|
|
6
|
+
// metadata-filter language in `upstashvector-filter.ts`. What is modeled here is Upstash Vector's
|
|
7
|
+
// REST protocol itself:
|
|
8
|
+
//
|
|
9
|
+
// POST /upsert body: one vector object OR an array of them
|
|
10
|
+
// POST /upsert/{ns} ... into a named namespace (namespaces are a PATH SUFFIX)
|
|
11
|
+
// POST /query body: one query object → {result:[...]}
|
|
12
|
+
// POST /query body: an ARRAY of query objects → queryMany (see below)
|
|
13
|
+
// POST /fetch /range /delete /update (+ their /{ns} forms)
|
|
14
|
+
// POST|DELETE /reset /reset/{ns} /reset?all
|
|
15
|
+
// GET|POST /info GET|POST /list-namespaces
|
|
16
|
+
// POST|DELETE /delete-namespace/{ns}
|
|
17
|
+
//
|
|
18
|
+
// ── FOUR THINGS A NAIVE TWIN GETS WRONG ───────────────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// 1. EVERY SDK CALL IS A **POST**, whatever the docs say the method is. `@upstash/vector@1.2.3`'s
|
|
21
|
+
// `HttpClient.request` hardcodes `method: "POST"` (confirmed in the installed dist), so although
|
|
22
|
+
// the docs spell `/reset` and `/delete-namespace` as `-X DELETE`, the real SDK never sends
|
|
23
|
+
// DELETE at all. A twin that accepted only the documented method would 405 the actual client.
|
|
24
|
+
// Both are accepted here.
|
|
25
|
+
//
|
|
26
|
+
// 2. NAMESPACES ARE A PATH SUFFIX, NOT A BODY FIELD. `index.namespace("ns").query(...)` becomes
|
|
27
|
+
// `POST /query/ns` — the namespace never appears in the JSON. A twin reading `body.namespace`
|
|
28
|
+
// would silently serve every namespaced request out of the default namespace, so every tenant
|
|
29
|
+
// would see every other tenant's vectors.
|
|
30
|
+
//
|
|
31
|
+
// 3. `/query` IS OVERLOADED ON BODY SHAPE. An OBJECT body is one query and answers
|
|
32
|
+
// `{"result":[...]}`; an ARRAY body is `queryMany` and answers a nested `{"result":[[...],...]}`
|
|
33
|
+
// — EXCEPT for a single-element array, which the real API answers FLAT. That is not a guess:
|
|
34
|
+
// `QueryManyCommand.exec` in the installed SDK re-nests it and its own comment says "When a
|
|
35
|
+
// single query is sent via queryMany, the API returns a flat array of results instead of a
|
|
36
|
+
// nested array." A twin that always nested would hand `queryMany([q])` a doubly-nested result.
|
|
37
|
+
//
|
|
38
|
+
// 4. AN UNMODELED ENDPOINT MUST 404, NEVER 200. `/upsert-data`, `/query-data` and the
|
|
39
|
+
// resumable-query family are real vendor surface this twin does not serve; each fails with the
|
|
40
|
+
// vendor's `{error,status}` envelope naming what is missing, so a caller can never mistake
|
|
41
|
+
// silence for success.
|
|
42
|
+
//
|
|
43
|
+
// GROUNDED (2026-08-19) — see spec-sources.json and the store module's header. Upstash publishes
|
|
44
|
+
// no literal text for most of its errors and this pack had no live index to probe, so the error
|
|
45
|
+
// STRINGS below are twin-authored except the dimension mismatch, which is quoted from the vendor's
|
|
46
|
+
// own upsert doc. What IS faithful and asserted: the `{result}` / `{error,status}` envelope, the
|
|
47
|
+
// status codes, the routing table, and every response body shape.
|
|
48
|
+
import {
|
|
49
|
+
DEFAULT_NAMESPACE,
|
|
50
|
+
IndexSpace,
|
|
51
|
+
ReadOnlyError,
|
|
52
|
+
VectorApiError,
|
|
53
|
+
UPSTASHVECTOR_RESOURCE_TYPES,
|
|
54
|
+
type SimilarityFunction,
|
|
55
|
+
type VectorContext,
|
|
56
|
+
} from './upstashvector-store.ts';
|
|
57
|
+
|
|
58
|
+
export type UpstashVectorRequest = {
|
|
59
|
+
method: string;
|
|
60
|
+
path: string;
|
|
61
|
+
body?: string;
|
|
62
|
+
headers?: Record<string, string>;
|
|
63
|
+
occurredAt?: string;
|
|
64
|
+
root?: string;
|
|
65
|
+
readOnly?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The token this twin accepts. When set, the request's credential must match it EXACTLY.
|
|
68
|
+
* When omitted the twin accepts any NON-EMPTY credential and still 401s a missing/blank one —
|
|
69
|
+
* the honest default for a local twin whose whole point is that you hold no real secret.
|
|
70
|
+
*/
|
|
71
|
+
token?: string;
|
|
72
|
+
/** The dimension this index was created with. Omit to let the first upsert lock one in. */
|
|
73
|
+
dimension?: number;
|
|
74
|
+
/** The metric this index was created with. Defaults to COSINE. */
|
|
75
|
+
similarityFunction?: SimilarityFunction;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type UpstashVectorResponse = { status: number; body: unknown; headers?: Record<string, string> };
|
|
79
|
+
|
|
80
|
+
/** Response headers. `content-type` is what the SDK's `res.json()` requires. */
|
|
81
|
+
const BASE_HEADERS: Record<string, string> = {
|
|
82
|
+
'content-type': 'application/json; charset=utf-8',
|
|
83
|
+
'access-control-allow-credentials': 'true',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The 401 body — VERBATIM from upstash.com/docs/vector/api/get-started, whose error example is
|
|
88
|
+
* exactly `{"error": "Unauthorized: Invalid auth token", "status": 401}`.
|
|
89
|
+
*
|
|
90
|
+
* (§9 round 1 caught this pack UNDER-claiming: an earlier draft used its own wording and declared
|
|
91
|
+
* the vendor's text unpublished, when the get-started page publishes it. Note the capital `I` in
|
|
92
|
+
* "Invalid" — matching it exactly is the difference between a consumer's error assertion passing
|
|
93
|
+
* against this twin and only passing against the real service. This is now one of TWO strings in
|
|
94
|
+
* the pack quoted verbatim from the vendor, alongside the dimension-mismatch 422.)
|
|
95
|
+
*/
|
|
96
|
+
export const UPSTASH_VECTOR_UNAUTHORIZED_ERROR = 'Unauthorized: Invalid auth token';
|
|
97
|
+
|
|
98
|
+
/** Every endpoint this twin routes, and the ones it deliberately refuses. */
|
|
99
|
+
export type UpstashVectorCommand =
|
|
100
|
+
| 'upsert' | 'query' | 'fetch' | 'range' | 'delete' | 'update' | 'reset'
|
|
101
|
+
| 'info' | 'list-namespaces' | 'delete-namespace';
|
|
102
|
+
|
|
103
|
+
/** Real vendor endpoints this twin does NOT serve, each with the manifest gap that tracks it. */
|
|
104
|
+
const UNMODELED_ENDPOINTS: Record<string, string> = {
|
|
105
|
+
'upsert-data': 'upstashvector.embedding.upsert_data',
|
|
106
|
+
'query-data': 'upstashvector.embedding.query_data',
|
|
107
|
+
'resumable-query': 'upstashvector.resumable.start',
|
|
108
|
+
'resumable-query-data': 'upstashvector.resumable.start',
|
|
109
|
+
'resumable-query-next': 'upstashvector.resumable.next',
|
|
110
|
+
'resumable-query-end': 'upstashvector.resumable.stop',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const MODELED: ReadonlySet<string> = new Set<UpstashVectorCommand>([
|
|
114
|
+
'upsert', 'query', 'fetch', 'range', 'delete', 'update', 'reset', 'info', 'list-namespaces', 'delete-namespace',
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
function lowerHeaders(h: Record<string, string> | undefined): Record<string, string> {
|
|
118
|
+
const out: Record<string, string> = {};
|
|
119
|
+
for (const [k, v] of Object.entries(h ?? {})) out[k.toLowerCase()] = v;
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract the caller's credential from `Authorization: Bearer <token>` (what the SDK sends) or a
|
|
125
|
+
* bare `Authorization: <token>`.
|
|
126
|
+
*
|
|
127
|
+
* The optional-group regex matters: `Authorization: Bearer ` — the scheme with an EMPTY token —
|
|
128
|
+
* trims to the bare word "Bearer". A `/^bearer\s+(.*)$/` pattern does not match it, so a naive
|
|
129
|
+
* implementation falls through and treats the literal string "Bearer" as the credential,
|
|
130
|
+
* authenticating a request that carries no token at all.
|
|
131
|
+
*/
|
|
132
|
+
export function extractUpstashVectorCredential(headers: Record<string, string> | undefined): string | null {
|
|
133
|
+
const raw = lowerHeaders(headers).authorization;
|
|
134
|
+
if (raw === undefined) return null;
|
|
135
|
+
const trimmed = raw.trim();
|
|
136
|
+
const bearer = /^bearer(?:\s+(.*))?$/i.exec(trimmed);
|
|
137
|
+
const token = bearer ? (bearer[1] ?? '').trim() : trimmed;
|
|
138
|
+
return token === '' ? null : token;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The parsed shape of a request path: which command, and which namespace. */
|
|
142
|
+
export type UpstashVectorRoute = { command: string; namespace: string };
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Split `/query/my-ns` into `{command:'query', namespace:'my-ns'}`.
|
|
146
|
+
*
|
|
147
|
+
* Everything after the first segment is the namespace, percent-DECODED and re-joined — a namespace
|
|
148
|
+
* is an arbitrary string, so one containing a `/` must survive the round trip rather than being
|
|
149
|
+
* truncated at the first slash into a DIFFERENT namespace's data.
|
|
150
|
+
*/
|
|
151
|
+
export function routeUpstashVectorPath(path: string): UpstashVectorRoute {
|
|
152
|
+
const clean = (path.split('?')[0] ?? '/').replace(/^\/+/, '');
|
|
153
|
+
const segments = clean.split('/');
|
|
154
|
+
const command = decodeSegment(segments[0] ?? '').toLowerCase();
|
|
155
|
+
const namespace = segments.length > 1 ? segments.slice(1).map(decodeSegment).join('/') : DEFAULT_NAMESPACE;
|
|
156
|
+
return { command, namespace };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function decodeSegment(seg: string): string {
|
|
160
|
+
try {
|
|
161
|
+
return decodeURIComponent(seg);
|
|
162
|
+
} catch {
|
|
163
|
+
return seg; // a malformed escape is passed through rather than turned into a 500
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function handleUpstashVectorTwinRequest(req: UpstashVectorRequest): Promise<UpstashVectorResponse> {
|
|
168
|
+
const method = req.method.toUpperCase();
|
|
169
|
+
const [rawPath, rawQuery] = req.path.split('?');
|
|
170
|
+
const query = new URLSearchParams(rawQuery ?? '');
|
|
171
|
+
const headers = lowerHeaders(req.headers);
|
|
172
|
+
const occurredAt = req.occurredAt ?? new Date().toISOString();
|
|
173
|
+
|
|
174
|
+
// CORS preflight. Answered before auth — a browser preflight carries no Authorization header by
|
|
175
|
+
// definition, so 401ing it would make the twin unusable from any browser client.
|
|
176
|
+
if (method === 'OPTIONS') {
|
|
177
|
+
return {
|
|
178
|
+
status: 200,
|
|
179
|
+
body: null,
|
|
180
|
+
headers: {
|
|
181
|
+
'access-control-allow-origin': headers.origin ?? '*',
|
|
182
|
+
'access-control-allow-methods': 'GET,POST,PUT,DELETE,HEAD,OPTIONS',
|
|
183
|
+
'access-control-allow-headers': headers['access-control-request-headers'] ?? 'authorization,content-type',
|
|
184
|
+
'access-control-allow-credentials': 'true',
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// upstash.com/docs/vector/api/get-started documents 405 for an unsupported method. DELETE is
|
|
190
|
+
// included because the docs spell `/reset` and `/delete-namespace` with `-X DELETE`.
|
|
191
|
+
if (!['GET', 'POST', 'PUT', 'HEAD', 'DELETE'].includes(method)) {
|
|
192
|
+
return fail(405, `Method Not Allowed: ${method}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── auth, before anything else ─────────────────────────────────────────────────────────────
|
|
196
|
+
const credential = extractUpstashVectorCredential(req.headers);
|
|
197
|
+
const authorized = req.token === undefined ? credential !== null : credential === req.token;
|
|
198
|
+
if (!authorized) return fail(401, UPSTASH_VECTOR_UNAUTHORIZED_ERROR);
|
|
199
|
+
|
|
200
|
+
const { command, namespace } = routeUpstashVectorPath(rawPath ?? '/');
|
|
201
|
+
|
|
202
|
+
if (command === '') return fail(404, 'Not Found: no endpoint at /');
|
|
203
|
+
if (!MODELED.has(command)) {
|
|
204
|
+
const gap = UNMODELED_ENDPOINTS[command];
|
|
205
|
+
// A REAL vendor endpoint this twin has not built fails 404 naming the filed gap; anything else
|
|
206
|
+
// is simply not an Upstash Vector endpoint. Neither can ever be mistaken for a success.
|
|
207
|
+
return fail(404, gap !== undefined
|
|
208
|
+
? `upstashvector: /${command} is real Upstash Vector surface this twin does not model yet (${gap} is the filed gap)`
|
|
209
|
+
: `Not Found: /${command} is not an Upstash Vector endpoint`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// GET AND HEAD ARE SAFE METHODS, so neither may mutate anything.
|
|
213
|
+
//
|
|
214
|
+
// §9 round 1 found the worst bug in this file: the HEAD check used to live AFTER `runCommand`
|
|
215
|
+
// and `flush()`, so `HEAD /reset` emptied the index and `HEAD /delete` removed vectors — then
|
|
216
|
+
// suppressed the body and answered a read-shaped, body-less 200. A destructive operation
|
|
217
|
+
// disguised as a read is the worst available failure mode. Forcing the request read-only means
|
|
218
|
+
// a HEAD to a write endpoint takes the ordinary 405 read-only path, while `HEAD /info` still
|
|
219
|
+
// answers 200 with no body.
|
|
220
|
+
//
|
|
221
|
+
// GET is included for the same reason (RFC 9110 defines both as safe): caches, prefetchers,
|
|
222
|
+
// crawlers and link-checkers issue GET freely, and no documented Upstash Vector client ever
|
|
223
|
+
// sends GET to a write endpoint — the SDK POSTs everything, and the docs use POST or DELETE. So
|
|
224
|
+
// the cost of refusing is zero for every real caller, while the cost of allowing it is an index
|
|
225
|
+
// destroyed by something that thought it was reading. What the REAL service does with
|
|
226
|
+
// `GET /reset` is UNVERIFIED (no live index to probe) and is filed as
|
|
227
|
+
// `upstashvector.protocol.get_on_a_write_endpoint`; this twin takes the safe side deliberately
|
|
228
|
+
// rather than guessing the destructive one.
|
|
229
|
+
const readOnly = (req.readOnly ?? false) || method === 'HEAD' || method === 'GET';
|
|
230
|
+
|
|
231
|
+
const ctx: VectorContext = {
|
|
232
|
+
occurredAt,
|
|
233
|
+
...(req.root !== undefined ? { root: req.root } : {}),
|
|
234
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
235
|
+
...(req.dimension !== undefined ? { dimension: req.dimension } : {}),
|
|
236
|
+
...(req.similarityFunction !== undefined ? { similarityFunction: req.similarityFunction } : {}),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const space = new IndexSpace(ctx);
|
|
241
|
+
const result = await runCommand(space, command as UpstashVectorCommand, namespace, req.body, query);
|
|
242
|
+
await space.flush();
|
|
243
|
+
// HEAD answers with no body at all.
|
|
244
|
+
if (method === 'HEAD') return { status: 200, body: null, headers: { ...BASE_HEADERS } };
|
|
245
|
+
return { status: 200, body: { result }, headers: { ...BASE_HEADERS } };
|
|
246
|
+
} catch (e) {
|
|
247
|
+
if (e instanceof ReadOnlyError) {
|
|
248
|
+
// Distinguish the two reasons a write can be refused. §9 round 2: a `HEAD /upsert` against a
|
|
249
|
+
// perfectly writable twin used to answer "this twin was started read-only", which is simply
|
|
250
|
+
// false and sends the reader hunting for a flag they never set.
|
|
251
|
+
return fail(405, readOnly && !(req.readOnly ?? false)
|
|
252
|
+
? `upstashvector: ${method} is a safe method and cannot modify the index — use POST (or PUT/DELETE where the endpoint documents it)`
|
|
253
|
+
: e.message);
|
|
254
|
+
}
|
|
255
|
+
if (e instanceof VectorApiError) return fail(e.status, e.message);
|
|
256
|
+
// LAST-RESORT ENVELOPE. A twin's caller must always get a response it can reason about, and an
|
|
257
|
+
// internal fault must be VISIBLE rather than silent — an unhandled rejection would leave the
|
|
258
|
+
// request with no HTTP answer at all. Surfaced as a loud, twin-attributed 500.
|
|
259
|
+
return fail(500, `upstashvector: internal fault handling /${command} — ${e instanceof Error ? e.message : String(e)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The vendor's error envelope: `{"error": "...", "status": N}` (upsert doc's own 422 example). */
|
|
264
|
+
function fail(status: number, error: string): UpstashVectorResponse {
|
|
265
|
+
return { status, body: { error, status }, headers: { ...BASE_HEADERS } };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function runCommand(
|
|
269
|
+
space: IndexSpace,
|
|
270
|
+
command: UpstashVectorCommand,
|
|
271
|
+
namespace: string,
|
|
272
|
+
rawBody: string | undefined,
|
|
273
|
+
query: URLSearchParams,
|
|
274
|
+
): Promise<unknown> {
|
|
275
|
+
switch (command) {
|
|
276
|
+
case 'info':
|
|
277
|
+
return space.info();
|
|
278
|
+
case 'list-namespaces':
|
|
279
|
+
return space.listNamespaces();
|
|
280
|
+
case 'delete-namespace':
|
|
281
|
+
space.deleteNamespace(namespace);
|
|
282
|
+
return 'Success';
|
|
283
|
+
case 'reset':
|
|
284
|
+
// `?all` is a BARE query flag — `ResetCommand` builds the literal string `reset?all`, so the
|
|
285
|
+
// parameter has an empty value and `query.get('all')` is `''`, not `null`. Testing
|
|
286
|
+
// `.has('all')` is what makes the SDK's own spelling work.
|
|
287
|
+
return space.reset(query.has('all') ? { all: true } : { namespace });
|
|
288
|
+
case 'upsert': {
|
|
289
|
+
const body = parseJsonBody(rawBody);
|
|
290
|
+
// The vendor accepts a single object OR an array (its own doc shows both curl forms).
|
|
291
|
+
return space.upsert(namespace, Array.isArray(body) ? body : [body]);
|
|
292
|
+
}
|
|
293
|
+
case 'update':
|
|
294
|
+
return space.update(namespace, parseJsonBody(rawBody));
|
|
295
|
+
case 'fetch':
|
|
296
|
+
return space.fetch(namespace, parseJsonBody(rawBody));
|
|
297
|
+
case 'range':
|
|
298
|
+
return space.range(namespace, parseJsonBody(rawBody));
|
|
299
|
+
case 'delete':
|
|
300
|
+
return space.delete(namespace, parseJsonBody(rawBody));
|
|
301
|
+
case 'query': {
|
|
302
|
+
const body = parseJsonBody(rawBody);
|
|
303
|
+
if (!Array.isArray(body)) return space.query(namespace, body);
|
|
304
|
+
// queryMany. See the module header, point 3: a ONE-element batch answers FLAT, which is the
|
|
305
|
+
// shape the installed SDK's own normalization proves the real API returns.
|
|
306
|
+
if (body.length === 0) throw new VectorApiError('upstashvector: query batch must not be empty', 400);
|
|
307
|
+
const results = body.map((one) => space.query(namespace, one));
|
|
308
|
+
return body.length === 1 ? results[0]! : results;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Parse a JSON request body, refusing what the vendor refuses. */
|
|
314
|
+
function parseJsonBody(raw: string | undefined): unknown {
|
|
315
|
+
if (raw === undefined || raw.trim() === '') throw new VectorApiError('upstashvector: request body is required', 400);
|
|
316
|
+
try {
|
|
317
|
+
return JSON.parse(raw);
|
|
318
|
+
} catch {
|
|
319
|
+
throw new VectorApiError('upstashvector: request body is not valid JSON', 400);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── the self-referential conformance snapshot ──────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
export type UpstashVectorTwinSnapshot = {
|
|
326
|
+
resourceTypes: readonly string[];
|
|
327
|
+
implementedEndpoints: readonly string[];
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export function upstashvectorTwinSnapshot(): UpstashVectorTwinSnapshot {
|
|
331
|
+
return {
|
|
332
|
+
resourceTypes: UPSTASHVECTOR_RESOURCE_TYPES,
|
|
333
|
+
implementedEndpoints: [
|
|
334
|
+
'POST /upsert[/{namespace}] (one vector object or an array of them)',
|
|
335
|
+
'POST /query[/{namespace}] (an object = one query; an array = queryMany)',
|
|
336
|
+
'POST /fetch[/{namespace}] (by ids — positionally aligned, null for a miss — or by prefix)',
|
|
337
|
+
'POST /range[/{namespace}] (offset cursor, nextCursor "" when exhausted)',
|
|
338
|
+
'POST /update[/{namespace}] ({updated:0|1}, metadataUpdateMode OVERWRITE|PATCH)',
|
|
339
|
+
'POST|DELETE /delete[/{namespace}] (by ids, prefix or metadata filter)',
|
|
340
|
+
'POST|DELETE /reset[/{namespace}] and /reset?all',
|
|
341
|
+
'GET|POST /info (counts + configuration + the per-namespace map)',
|
|
342
|
+
'GET|POST /list-namespaces',
|
|
343
|
+
'POST|DELETE /delete-namespace/{namespace}',
|
|
344
|
+
'OPTIONS (CORS preflight)',
|
|
345
|
+
],
|
|
346
|
+
};
|
|
347
|
+
}
|