@instruments/materialgraph 0.9.1 → 0.11.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/brands-client.d.ts +48 -0
- package/brands-client.js +45 -0
- package/browse-types.d.ts +147 -0
- package/browse-types.js +1 -0
- package/by-colour-types.d.ts +105 -5
- package/client-browse.d.ts +13 -3
- package/client-browse.js +12 -2
- package/client-exploration.d.ts +9 -0
- package/client-exploration.js +7 -0
- package/client-source-material-comments.d.ts +51 -0
- package/client-source-material-comments.js +40 -0
- package/client.d.ts +64 -71
- package/client.js +90 -78
- package/colour-aggregate-types.d.ts +58 -0
- package/colours-client.d.ts +8 -2
- package/colours-client.js +15 -2
- package/composition-client.d.ts +33 -0
- package/composition-client.js +27 -0
- package/composition-types.d.ts +46 -0
- package/coordinate-client.d.ts +23 -0
- package/coordinate-client.js +25 -0
- package/errors.js +24 -8
- package/exploration-types.d.ts +110 -0
- package/exploration-types.js +1 -0
- package/fan-deck-types.d.ts +42 -0
- package/fan-deck-types.js +9 -0
- package/http-transport.js +24 -5
- package/index.d.ts +7 -3
- package/index.js +2 -2
- package/match-stream-types.d.ts +1 -1
- package/match-stream-types.js +16 -3
- package/match-stream.js +14 -4
- package/material-types.d.ts +1 -1
- package/operations.d.ts +1 -1
- package/operations.js +187 -26
- package/package.json +1 -1
- package/palette-search-types.d.ts +31 -0
- package/resolve-client.d.ts +22 -3
- package/resolve-client.js +31 -4
- package/resolve-types.d.ts +70 -1
- package/run-events.d.ts +1 -1
- package/run-events.js +4 -1
- package/run-types.d.ts +132 -0
- package/runs.d.ts +35 -1
- package/runs.js +69 -5
- package/schedule-types.d.ts +128 -0
- package/schema-resource-types.d.ts +1 -1
- package/search-refinement-types.d.ts +39 -4
- package/search-request-types.d.ts +172 -0
- package/search-request-types.js +1 -0
- package/search-result-validation.js +21 -4
- package/slot-pool-types.d.ts +370 -0
- package/slot-pool-types.js +1 -0
- package/source-material-validation.d.ts +5 -1
- package/source-material-validation.js +114 -5
- package/spec-facet-types.d.ts +53 -0
- package/spec-facet-types.js +7 -0
- package/specifications.d.ts +26 -0
- package/specifications.js +32 -0
- package/sse.js +3 -1
- package/suggest-types.js +3 -1
- package/types.d.ts +176 -216
- package/validation.d.ts +12 -0
- package/validation.js +23 -0
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* nothing at run time. This is the run-time half, and it must ACCEPT and REJECT
|
|
12
12
|
* exactly what the schema does — pinned by `source-material-parity.test.ts`.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* THREE CONTRACT PROPERTIES IT ENFORCES, each easy to lose in a refactor:
|
|
15
15
|
* 1. `analyses` is REQUIRED and an array on every file. `[]` means "nothing
|
|
16
16
|
* computed yet"; an absent field is a broken response, not an empty one.
|
|
17
17
|
* Preprocessing is manual today and automatic later — only the trigger
|
|
18
18
|
* changes, never this contract.
|
|
19
|
-
* 2. `
|
|
19
|
+
* 2. `comments` is REQUIRED and an array on every file, and every root comment
|
|
20
|
+
* carries a REQUIRED `replies` array. Same distinction: "nobody said
|
|
21
|
+
* anything about this file" is a state the client draws, and an absent
|
|
22
|
+
* field is a malformed response.
|
|
23
|
+
* 3. `result` is a JSON record all the way down (`z.json()`), not `unknown`.
|
|
20
24
|
* See `requireJsonValue` for why the schema is that strict and why this
|
|
21
25
|
* mirror cannot actually fire over the wire.
|
|
22
26
|
*
|
|
@@ -30,7 +34,7 @@
|
|
|
30
34
|
* permanent contract, and promoting it later is additive.
|
|
31
35
|
*/
|
|
32
36
|
import { MaterialGraphError } from "./errors.js";
|
|
33
|
-
import { requireArray, requireEnum, requireSafeInteger, requireJsonRecord, requireNonEmptyString, requireRecord, requireString, } from "./validation.js";
|
|
37
|
+
import { requireArray, requireBoolean, requireEnum, requireSafeInteger, requireJsonRecord, requireNonEmptyString, requireNullableTrimmedBoundedString, requireRecord, requireString, requireTrimmedBoundedString, } from "./validation.js";
|
|
34
38
|
/** `sourceImageAnalysisKindSchema` members, verbatim. */
|
|
35
39
|
const ANALYSIS_KINDS = ["scene_match"];
|
|
36
40
|
/** `sourceImageAnalysisStatusSchema` members, verbatim. */
|
|
@@ -61,12 +65,67 @@ function checkAnalysis(value, what, fail) {
|
|
|
61
65
|
analysedAt: requireNonEmptyString(record.analysedAt, `${what}.analysedAt`, fail),
|
|
62
66
|
};
|
|
63
67
|
}
|
|
68
|
+
/** `sourceMaterialCommentAuthorSchema`'s `attribution` members, verbatim. */
|
|
69
|
+
const COMMENT_ATTRIBUTIONS = ["reported"];
|
|
70
|
+
/** The canonical cap on both author fields: `.trim().min(1).max(160)`. */
|
|
71
|
+
const AUTHOR_FIELD_MAX = 160;
|
|
72
|
+
/**
|
|
73
|
+
* A reply, and the shared half of a root comment.
|
|
74
|
+
*
|
|
75
|
+
* `body`, `commentId` and `createdAt` are `.min(1)` canonically — fields this
|
|
76
|
+
* endpoint introduces, where an empty value carries nothing a client could
|
|
77
|
+
* render. `author.organisation` is nullable, NOT optional: `null` is the answer
|
|
78
|
+
* for a note with no organisation, and an absent key is a malformed body.
|
|
79
|
+
*
|
|
80
|
+
* The author fields are `.trim().min(1).max(160)` canonically and are mirrored
|
|
81
|
+
* as exactly that. A plain non-empty check — which is what this used to do —
|
|
82
|
+
* disagreed with the schema in both directions: it accepted `" "`, which the
|
|
83
|
+
* schema rejects once trimmed, and accepted a name of any length, which the
|
|
84
|
+
* schema caps. A validator that is looser than the contract is a validator that
|
|
85
|
+
* lets the two drift without anything failing.
|
|
86
|
+
*/
|
|
87
|
+
function checkCommentBase(record, what, fail) {
|
|
88
|
+
const author = requireRecord(record.author, `${what}.author`, fail);
|
|
89
|
+
const recordedBy = requireRecord(record.recordedBy, `${what}.recordedBy`, fail);
|
|
90
|
+
return {
|
|
91
|
+
commentId: requireNonEmptyString(record.commentId, `${what}.commentId`, fail),
|
|
92
|
+
author: {
|
|
93
|
+
name: requireTrimmedBoundedString(author.name, `${what}.author.name`, AUTHOR_FIELD_MAX, fail),
|
|
94
|
+
organisation: requireNullableTrimmedBoundedString(author.organisation, `${what}.author.organisation`, AUTHOR_FIELD_MAX, fail),
|
|
95
|
+
// Closed here, matching the canonical `z.literal("reported")`, while the
|
|
96
|
+
// published type is `OpenEnum` so a newer server value keeps
|
|
97
|
+
// type-checking — the same split the analysis enums make.
|
|
98
|
+
attribution: requireEnum(author.attribution, COMMENT_ATTRIBUTIONS, `${what}.author.attribution`, fail),
|
|
99
|
+
},
|
|
100
|
+
recordedBy: {
|
|
101
|
+
principalId: requireNonEmptyString(recordedBy.principalId, `${what}.recordedBy.principalId`, fail),
|
|
102
|
+
},
|
|
103
|
+
body: requireNonEmptyString(record.body, `${what}.body`, fail),
|
|
104
|
+
createdAt: requireNonEmptyString(record.createdAt, `${what}.createdAt`, fail),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function checkComment(value, what, fail) {
|
|
108
|
+
const record = requireRecord(value, what, fail);
|
|
109
|
+
// Same reasoning as `analyses`: no `.default([])` canonically, so an absent
|
|
110
|
+
// `replies` is malformed rather than empty.
|
|
111
|
+
const replies = requireArray(record.replies, `${what}.replies`, fail);
|
|
112
|
+
return {
|
|
113
|
+
...checkCommentBase(record, what, fail),
|
|
114
|
+
replies: replies.map((reply, index) => checkCommentBase(requireRecord(reply, `${what}.replies[${index}]`, fail), `${what}.replies[${index}]`, fail)),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
64
117
|
function checkFile(value, what, fail) {
|
|
65
118
|
const record = requireRecord(value, what, fail);
|
|
66
119
|
// NOT `requireDefaultedArray`: the schema has no `.default([])`, so an absent
|
|
67
120
|
// `analyses` is a malformed body rather than an empty one. Keeping that
|
|
68
|
-
// distinction is the whole point of the always-an-array contract.
|
|
121
|
+
// distinction is the whole point of the always-an-array contract. `comments`
|
|
122
|
+
// is required for the same reason: "nobody has said anything" and "the server
|
|
123
|
+
// forgot to tell you" are different answers.
|
|
69
124
|
const analyses = requireArray(record.analyses, `${what}.analyses`, fail);
|
|
125
|
+
const comments = requireArray(record.comments, `${what}.comments`, fail);
|
|
126
|
+
// Required, not defaulted: "you are seeing the whole thread" is a claim the
|
|
127
|
+
// server makes or does not, never one a client should infer from a length.
|
|
128
|
+
const commentsTruncated = requireBoolean(record.commentsTruncated, `${what}.commentsTruncated`, fail);
|
|
70
129
|
// `requireString`, NOT `requireNonEmptyString`, for the fields this endpoint
|
|
71
130
|
// INHERITS from the shared source-material contract (`sourceId`, `sha256`,
|
|
72
131
|
// `url` — plus `batchId`/`createdAt` below). Those are plain `z.string()`
|
|
@@ -91,9 +150,13 @@ function checkFile(value, what, fail) {
|
|
|
91
150
|
// fields is a canonical change to a schema shared with the upload path, and
|
|
92
151
|
// belongs in its own ticket with this mirror following after.
|
|
93
152
|
bytes: requireSafeInteger(record.bytes, `${what}.bytes`, fail),
|
|
94
|
-
pages: record.pages === null
|
|
153
|
+
pages: record.pages === null
|
|
154
|
+
? null
|
|
155
|
+
: requireSafeInteger(record.pages, `${what}.pages`, fail),
|
|
95
156
|
url: requireString(record.url, `${what}.url`, fail),
|
|
96
157
|
analyses: analyses.map((analysis, index) => checkAnalysis(analysis, `${what}.analyses[${index}]`, fail)),
|
|
158
|
+
comments: comments.map((comment, index) => checkComment(comment, `${what}.comments[${index}]`, fail)),
|
|
159
|
+
commentsTruncated,
|
|
97
160
|
};
|
|
98
161
|
}
|
|
99
162
|
/** Validate a batch-detail body, returning it typed, or throw explaining why. */
|
|
@@ -107,3 +170,49 @@ export function checkSourceMaterialBatchDetail(value, batchId) {
|
|
|
107
170
|
sources: sources.map((file, index) => checkFile(file, `body.sources[${index}]`, fail)),
|
|
108
171
|
};
|
|
109
172
|
}
|
|
173
|
+
function commentsFailure(verb, sourceId) {
|
|
174
|
+
return (reason) => MaterialGraphError.unexpected(`${verb} .../sources/${sourceId}/comments returned a malformed body: ${reason}`);
|
|
175
|
+
}
|
|
176
|
+
/** Validate a comment-thread body, returning it typed, or throw explaining why. */
|
|
177
|
+
export function checkSourceMaterialCommentThread(value, sourceId) {
|
|
178
|
+
const fail = commentsFailure("GET", sourceId);
|
|
179
|
+
const record = requireRecord(value, "body", fail);
|
|
180
|
+
const comments = requireArray(record.comments, "body.comments", fail);
|
|
181
|
+
return {
|
|
182
|
+
sourceId: requireString(record.sourceId, "body.sourceId", fail),
|
|
183
|
+
comments: comments.map((comment, index) => checkComment(comment, `body.comments[${index}]`, fail)),
|
|
184
|
+
truncated: requireBoolean(record.truncated, "body.truncated", fail),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** `storedSourceMaterialCommentSchema`'s discriminator members, verbatim. */
|
|
188
|
+
const STORED_COMMENT_KINDS = ["root", "reply"];
|
|
189
|
+
/**
|
|
190
|
+
* Validate a stored comment, following its `kind` discriminator.
|
|
191
|
+
*
|
|
192
|
+
* The two branches are checked as the two DIFFERENT shapes they are: a root must
|
|
193
|
+
* carry `replies`, a reply must carry `parentCommentId` and must not be assumed
|
|
194
|
+
* to carry `replies` at all. Validating both as the root shape is exactly the
|
|
195
|
+
* defect this discriminated union was added to fix, and a mirror that did it
|
|
196
|
+
* would put the lie back on the client side.
|
|
197
|
+
*/
|
|
198
|
+
function checkStoredComment(value, what, fail) {
|
|
199
|
+
const record = requireRecord(value, what, fail);
|
|
200
|
+
const kind = requireEnum(record.kind, STORED_COMMENT_KINDS, `${what}.kind`, fail);
|
|
201
|
+
if (kind === "reply") {
|
|
202
|
+
return {
|
|
203
|
+
kind: "reply",
|
|
204
|
+
...checkCommentBase(record, what, fail),
|
|
205
|
+
parentCommentId: requireNonEmptyString(record.parentCommentId, `${what}.parentCommentId`, fail),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { kind: "root", ...checkComment(record, what, fail) };
|
|
209
|
+
}
|
|
210
|
+
/** Validate a posted-comment body, returning it typed, or throw explaining why. */
|
|
211
|
+
export function checkPostSourceMaterialCommentResult(value, sourceId) {
|
|
212
|
+
const fail = commentsFailure("POST", sourceId);
|
|
213
|
+
const record = requireRecord(value, "body", fail);
|
|
214
|
+
return {
|
|
215
|
+
sourceId: requireString(record.sourceId, "body.sourceId", fail),
|
|
216
|
+
comment: checkStoredComment(record.comment, "body.comment", fail),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface SpecFacetValue {
|
|
2
|
+
value: string;
|
|
3
|
+
variants: number;
|
|
4
|
+
brands: number;
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A NUMERIC facet's observed span and the coverage behind it.
|
|
9
|
+
*
|
|
10
|
+
* A range, not buckets — no bucketing scheme has been decided, and one invented
|
|
11
|
+
* on the wire would be treated as canonical by everyone who read it. Both bounds
|
|
12
|
+
* are INCLUSIVE, matching `specRange`.
|
|
13
|
+
*
|
|
14
|
+
* READ `variants` BEFORE TRUSTING A FILTER. It is how many variants carry a
|
|
15
|
+
* value that parses as a number, i.e. how much of the corpus a range can reach;
|
|
16
|
+
* a facet that exists says the attribute is queryable, not that it is populated.
|
|
17
|
+
* `unparseableVariants` is the honesty companion: values recorded here that are
|
|
18
|
+
* not numbers, which a range EXCLUDES rather than reading as zero.
|
|
19
|
+
*/
|
|
20
|
+
export interface SpecFacetRange {
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
variants: number;
|
|
24
|
+
unparseableVariants: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SpecFacet {
|
|
27
|
+
attributeId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
isArray: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* How this facet is queried. `token` — pick a value from `values` and send it
|
|
32
|
+
* as `attribute:value`. `numeric` — `values` is EMPTY and `range` carries the
|
|
33
|
+
* span; send inclusive bounds via `specRangeCriterion` instead.
|
|
34
|
+
*/
|
|
35
|
+
kind: "token" | "numeric" | (string & {});
|
|
36
|
+
brands: number;
|
|
37
|
+
/** EMPTY for a `numeric` facet, which has no token vocabulary. */
|
|
38
|
+
values: SpecFacetValue[];
|
|
39
|
+
/** Present ONLY on a `numeric` facet. */
|
|
40
|
+
range?: SpecFacetRange;
|
|
41
|
+
description?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SpecFacetsListResponse {
|
|
44
|
+
mode: "facets";
|
|
45
|
+
count: number;
|
|
46
|
+
facets: SpecFacet[];
|
|
47
|
+
}
|
|
48
|
+
export interface SpecFacetsDistributionResponse {
|
|
49
|
+
mode: "distribution";
|
|
50
|
+
attributeId: string;
|
|
51
|
+
count: number;
|
|
52
|
+
values: SpecFacetValue[];
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ── Specification facets + attribute values ─────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Hand-maintained mirror of the `SpecificationFacet` OpenAPI schema (the source
|
|
4
|
+
// of truth on the wire); the `sdk-openapi-parity` test gates this against the
|
|
5
|
+
// generated spec. Split out of `types.ts` to keep that file under its growth
|
|
6
|
+
// baseline; re-exported from `types.ts` so importers see no change.
|
|
7
|
+
export {};
|
package/specifications.d.ts
CHANGED
|
@@ -57,6 +57,32 @@ export declare function certificationProgramme(programme: string): Certification
|
|
|
57
57
|
* client.bySpec([specCriterion("primary_material", "textile"), specCriterion("fire_classifications", "astm_e84_class_a")]);
|
|
58
58
|
*/
|
|
59
59
|
export declare function specCriterion(attributeId: string, value: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* An `attribute:min..max` RANGE criterion string for `bySpec`, for a
|
|
62
|
+
* NUMBER-valued spec (`light_reflectance_value`, `nrc_rating`,
|
|
63
|
+
* `slip_resistance_dcof`, …). Pass the result in `bySpec`'s `ranges` option.
|
|
64
|
+
*
|
|
65
|
+
* BOTH BOUNDS ARE INCLUSIVE — `{ min: 60 }` matches a value of exactly 60 —
|
|
66
|
+
* because spec language is written that way ("LRV of 60 or more", "DCOF ≥ 0.42")
|
|
67
|
+
* and because inclusive-both-ends means adjacent ranges overlap at the join
|
|
68
|
+
* rather than leaving a gap that hides products.
|
|
69
|
+
*
|
|
70
|
+
* At least one bound is REQUIRED: a range with neither is a presence check, and
|
|
71
|
+
* this throws rather than sending a criterion the server would have to refuse.
|
|
72
|
+
* Discover which attributes take a range — and the span they actually occupy —
|
|
73
|
+
* from `specificationFacets()`: those are the facets with `kind: "numeric"`.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // light paints, every brand at once
|
|
77
|
+
* client.bySpec([], { ranges: [specRangeCriterion("light_reflectance_value", { min: 60 })] });
|
|
78
|
+
* @example
|
|
79
|
+
* // a mid-tone band
|
|
80
|
+
* specRangeCriterion("light_reflectance_value", { min: 30, max: 45 }); // "light_reflectance_value:30..45"
|
|
81
|
+
*/
|
|
82
|
+
export declare function specRangeCriterion(attributeId: string, bounds: {
|
|
83
|
+
min?: number;
|
|
84
|
+
max?: number;
|
|
85
|
+
}): string;
|
|
60
86
|
/**
|
|
61
87
|
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
62
88
|
* certification PROGRAMME across all four certification buckets.
|
package/specifications.js
CHANGED
|
@@ -65,6 +65,38 @@ export function certificationProgramme(programme) {
|
|
|
65
65
|
export function specCriterion(attributeId, value) {
|
|
66
66
|
return `${attributeId}:${value}`;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* An `attribute:min..max` RANGE criterion string for `bySpec`, for a
|
|
70
|
+
* NUMBER-valued spec (`light_reflectance_value`, `nrc_rating`,
|
|
71
|
+
* `slip_resistance_dcof`, …). Pass the result in `bySpec`'s `ranges` option.
|
|
72
|
+
*
|
|
73
|
+
* BOTH BOUNDS ARE INCLUSIVE — `{ min: 60 }` matches a value of exactly 60 —
|
|
74
|
+
* because spec language is written that way ("LRV of 60 or more", "DCOF ≥ 0.42")
|
|
75
|
+
* and because inclusive-both-ends means adjacent ranges overlap at the join
|
|
76
|
+
* rather than leaving a gap that hides products.
|
|
77
|
+
*
|
|
78
|
+
* At least one bound is REQUIRED: a range with neither is a presence check, and
|
|
79
|
+
* this throws rather than sending a criterion the server would have to refuse.
|
|
80
|
+
* Discover which attributes take a range — and the span they actually occupy —
|
|
81
|
+
* from `specificationFacets()`: those are the facets with `kind: "numeric"`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // light paints, every brand at once
|
|
85
|
+
* client.bySpec([], { ranges: [specRangeCriterion("light_reflectance_value", { min: 60 })] });
|
|
86
|
+
* @example
|
|
87
|
+
* // a mid-tone band
|
|
88
|
+
* specRangeCriterion("light_reflectance_value", { min: 30, max: 45 }); // "light_reflectance_value:30..45"
|
|
89
|
+
*/
|
|
90
|
+
export function specRangeCriterion(attributeId, bounds) {
|
|
91
|
+
const { min, max } = bounds;
|
|
92
|
+
if (min === undefined && max === undefined) {
|
|
93
|
+
throw new Error(`specRangeCriterion("${attributeId}") needs a min, a max, or both — a range with neither would match every recorded value. Use hasSpec() for a presence check.`);
|
|
94
|
+
}
|
|
95
|
+
if (min !== undefined && max !== undefined && min > max) {
|
|
96
|
+
throw new Error(`specRangeCriterion("${attributeId}") is inverted (min ${min} > max ${max}) and can only ever match nothing.`);
|
|
97
|
+
}
|
|
98
|
+
return `${attributeId}:${min ?? ""}..${max ?? ""}`;
|
|
99
|
+
}
|
|
68
100
|
/**
|
|
69
101
|
* An `attribute:value` criterion string for `bySpec` that filters by
|
|
70
102
|
* certification PROGRAMME across all four certification buckets.
|
package/sse.js
CHANGED
|
@@ -40,7 +40,9 @@ export class SseDecoder {
|
|
|
40
40
|
flush() {
|
|
41
41
|
if (this.#buffer.length > 0) {
|
|
42
42
|
// At end of stream a deferred trailing CR is a real terminator, not data.
|
|
43
|
-
const line = this.#buffer.endsWith("\r")
|
|
43
|
+
const line = this.#buffer.endsWith("\r")
|
|
44
|
+
? this.#buffer.slice(0, -1)
|
|
45
|
+
: this.#buffer;
|
|
44
46
|
this.#buffer = "";
|
|
45
47
|
const frame = this.#consumeLine(line);
|
|
46
48
|
if (frame)
|
package/suggest-types.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* Undefined members are dropped by the transport.
|
|
22
22
|
*/
|
|
23
23
|
export function suggestQuery(q, options) {
|
|
24
|
-
const kind = Array.isArray(options?.kind)
|
|
24
|
+
const kind = Array.isArray(options?.kind)
|
|
25
|
+
? options.kind.join(",")
|
|
26
|
+
: options?.kind;
|
|
25
27
|
return { q, limit: options?.limit, kind };
|
|
26
28
|
}
|