@findlane/server 0.21.0 → 0.23.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/dist/index.cjs CHANGED
@@ -265,6 +265,32 @@ class SearchServerClient {
265
265
  const { signal, visitor } = requestOptions(requestOptionsOrSignal);
266
266
  return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
267
267
  },
268
+ merchandising: {
269
+ list: async (signal) => (await request("/merchandising", "GET", undefined, signal)).arrangements,
270
+ get: (target, signal) => request(`/merchandising/arrangement?${new URLSearchParams(target)}`, "GET", undefined, signal),
271
+ set: (target, change, signal) => request("/merchandising/arrangement", "PUT", { ...target, ...change }, signal),
272
+ remove: (target, signal) => request(`/merchandising/arrangement?${new URLSearchParams(target)}`, "DELETE", undefined, signal)
273
+ },
274
+ images: {
275
+ list: (signal) => request("/images", "GET", undefined, signal),
276
+ upload: async (image, options = {}) => {
277
+ const response = await this.fetcher(`${this.baseUrl}${path}/images`, {
278
+ method: "POST",
279
+ signal: options.signal,
280
+ headers: {
281
+ authorization: `Bearer ${this.key}`,
282
+ "content-type": options.contentType ?? (image instanceof Blob && image.type ? image.type : "application/octet-stream")
283
+ },
284
+ body: image
285
+ });
286
+ const data = await response.json();
287
+ if (!response.ok)
288
+ throw new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
289
+ return data;
290
+ },
291
+ uploadFromUrl: (url, signal) => request("/images", "POST", { url }, signal),
292
+ delete: (key, signal) => request(`/images/${key}`, "DELETE", undefined, signal)
293
+ },
268
294
  lists: {
269
295
  get: (id, options = {}) => request(`/lists/${encodeURIComponent(id)}${options.limit ? `?limit=${options.limit}` : ""}`, "GET", undefined, options.signal),
270
296
  getMany: async (ids, options = {}) => {
@@ -319,7 +345,8 @@ class SearchServerClient {
319
345
  products: (options = {}) => report("products", options),
320
346
  productCounts: (options = {}) => report("product-counts", options),
321
347
  product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options),
322
- revenue: (options = {}) => report("revenue", options)
348
+ revenue: (options = {}) => report("revenue", options),
349
+ banners: (options = {}) => request(`/banners?range=${options.range ?? "30d"}`, "GET", undefined, options.signal)
323
350
  },
324
351
  upsert: (records, signal) => request("/records", "POST", { records }, signal),
325
352
  getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
package/dist/index.d.cts CHANGED
@@ -745,6 +745,92 @@ export type ServerEvent = {
745
745
  | {
746
746
  type: "cancel";
747
747
  orderID: string;
748
+ }
749
+ /** A rule's banner was clicked: its rule, as search responses give it with the banner, the search's
750
+ * query, and its place. */
751
+ | {
752
+ type: "bannerClick";
753
+ rule: string;
754
+ query?: string;
755
+ position?: number;
756
+ timestamp?: number;
757
+ };
758
+ /** A category page (a facet field's value, without search words), or a search's exact words. */
759
+ export type MerchandisingTarget = {
760
+ field: string;
761
+ value: string;
762
+ } | {
763
+ q: string;
764
+ };
765
+ export type MerchandisingBanner = {
766
+ image: string;
767
+ link?: string;
768
+ title?: string;
769
+ position?: number;
770
+ };
771
+ /** How a category page or search is arranged, as the dashboard's Merchandising page arranges it. */
772
+ export type Arrangement = {
773
+ target: MerchandisingTarget;
774
+ /** The rule that holds it; null when nothing is arranged. */
775
+ rule: string | null;
776
+ /** The products placed first, in order; null when the rule's pins have gaps, listed in `pins`. */
777
+ order: string[] | null;
778
+ pins: {
779
+ objectID: string;
780
+ position: number;
781
+ }[];
782
+ hidden: string[];
783
+ banner: MerchandisingBanner | null;
784
+ active: {
785
+ from?: string;
786
+ until?: string;
787
+ } | null;
788
+ enabled: boolean;
789
+ status: "live" | "scheduled" | "ended" | "off" | "none";
790
+ };
791
+ /** What to change: each field given replaces the current one; null clears the banner or the times. */
792
+ export type ArrangementChange = {
793
+ order?: string[];
794
+ hidden?: string[];
795
+ banner?: MerchandisingBanner | null;
796
+ active?: {
797
+ from?: string;
798
+ until?: string;
799
+ } | null;
800
+ enabled?: boolean;
801
+ };
802
+ /** An image uploaded for the index's banners, hosted by Findlane, and the rules showing it. */
803
+ export type UploadedImage = {
804
+ key: string;
805
+ url: string;
806
+ bytes: number;
807
+ contentType: string;
808
+ width: number | null;
809
+ height: number | null;
810
+ createdAt: string;
811
+ usedBy: string[];
812
+ };
813
+ export type ImageLibrary = {
814
+ images: UploadedImage[];
815
+ used: {
816
+ files: number;
817
+ bytes: number;
818
+ };
819
+ limits: {
820
+ fileBytes: number;
821
+ files: number;
822
+ bytes: number;
823
+ };
824
+ };
825
+ /** Each banner's shown and clicks by its rule; `analytics` says whether they could be read. */
826
+ export type BannerStatsReport = {
827
+ days: number;
828
+ banners: {
829
+ rule: string;
830
+ shown: number;
831
+ clicks: number;
832
+ }[];
833
+ analytics: "ok" | "off" | "unavailable";
748
834
  };
749
835
  /** A past order for importOrders; `timestamp` is when it was placed (ms, a Date, or an ISO string). */
750
836
  export type Order = {
@@ -1039,6 +1125,39 @@ export declare class SearchServerClient {
1039
1125
  /** The index's lists: rows of products, such as Popular, and facet fields' values for menus, built ahead
1040
1126
  * and served from a cache, so reading them isn't counted as searches. Reading needs the `search` scope;
1041
1127
  * setting them up needs `write`. */
1128
+ /** Merchandising (0.23.0): category pages and searches arranged as the dashboard's Merchandising page does:
1129
+ * the products placed first, hidden products, a banner, and when. Saved as the page's rule within seconds,
1130
+ * keeping anything else the rule does. Reading needs the `search` or `write` scope; changing, `write`. */
1131
+ merchandising: {
1132
+ /** Every arranged category page and search. */
1133
+ list: (signal?: AbortSignal) => Promise<Arrangement[]>;
1134
+ /** One page's or search's arrangement; status "none" when nothing is arranged. */
1135
+ get: (target: MerchandisingTarget, signal?: AbortSignal) => Promise<Arrangement>;
1136
+ /** Changes one: `order` up to 20 objectIDs placed first, `hidden` up to 100, `banner` ({ image, link?, title?,
1137
+ * position? } or null), `active` ({ from?, until? } or null), `enabled`. Fields left out stay as they are. */
1138
+ set: (target: MerchandisingTarget, change: ArrangementChange, signal?: AbortSignal) => Promise<Arrangement>;
1139
+ /** Stops arranging one: its order, hidden products and banner go; the rule's other actions stay. */
1140
+ remove: (target: MerchandisingTarget, signal?: AbortSignal) => Promise<Arrangement>;
1141
+ };
1142
+ /** Images for the index's banners, hosted by Findlane at https://images.findlane.dev (0.23.0): JPEG, PNG,
1143
+ * WebP or AVIF, up to 2 MB each, 50 images and 100 MB an index. An image no rule shows is deleted a week
1144
+ * later. Uploading and deleting need the `write` scope. */
1145
+ images: {
1146
+ list: (signal?: AbortSignal) => Promise<ImageLibrary>;
1147
+ /** Uploads an image's bytes; the same image again gives the same URL. */
1148
+ upload: (image: Blob | ArrayBuffer | Uint8Array, options?: {
1149
+ contentType?: string;
1150
+ signal?: AbortSignal;
1151
+ }) => Promise<UploadedImage & {
1152
+ error?: string;
1153
+ }>;
1154
+ /** Uploads the image at an https address, which Findlane fetches. */
1155
+ uploadFromUrl: (url: string, signal?: AbortSignal) => Promise<UploadedImage>;
1156
+ /** Deletes an image by its key; refused (409) while a rule's banner shows it. */
1157
+ delete: (key: string, signal?: AbortSignal) => Promise<{
1158
+ deleted: true;
1159
+ }>;
1160
+ };
1042
1161
  lists: {
1043
1162
  /** One list by its ID; throws a FindlaneError with status 404 when there's none. `limit` shows fewer. */
1044
1163
  get: <R = T>(id: string, options?: {
@@ -1127,6 +1246,12 @@ export declare class SearchServerClient {
1127
1246
  /** Revenue from search: orders and add-to-carts of products found through search, per query and per
1128
1247
  * product (0.20.0). */
1129
1248
  revenue: (options?: Omit<AnalyticsOptions, "source">) => Promise<RevenueReport>;
1249
+ /** Each banner's shown (shoppers' searches that showed it, cache answers included) and clicks, by its rule,
1250
+ * over the last 7, 30 (by default) or 90 days (0.23.0). */
1251
+ banners: (options?: {
1252
+ range?: "7d" | "30d" | "90d";
1253
+ signal?: AbortSignal;
1254
+ }) => Promise<BannerStatsReport>;
1130
1255
  };
1131
1256
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
1132
1257
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
package/dist/index.d.ts CHANGED
@@ -745,6 +745,92 @@ export type ServerEvent = {
745
745
  | {
746
746
  type: "cancel";
747
747
  orderID: string;
748
+ }
749
+ /** A rule's banner was clicked: its rule, as search responses give it with the banner, the search's
750
+ * query, and its place. */
751
+ | {
752
+ type: "bannerClick";
753
+ rule: string;
754
+ query?: string;
755
+ position?: number;
756
+ timestamp?: number;
757
+ };
758
+ /** A category page (a facet field's value, without search words), or a search's exact words. */
759
+ export type MerchandisingTarget = {
760
+ field: string;
761
+ value: string;
762
+ } | {
763
+ q: string;
764
+ };
765
+ export type MerchandisingBanner = {
766
+ image: string;
767
+ link?: string;
768
+ title?: string;
769
+ position?: number;
770
+ };
771
+ /** How a category page or search is arranged, as the dashboard's Merchandising page arranges it. */
772
+ export type Arrangement = {
773
+ target: MerchandisingTarget;
774
+ /** The rule that holds it; null when nothing is arranged. */
775
+ rule: string | null;
776
+ /** The products placed first, in order; null when the rule's pins have gaps, listed in `pins`. */
777
+ order: string[] | null;
778
+ pins: {
779
+ objectID: string;
780
+ position: number;
781
+ }[];
782
+ hidden: string[];
783
+ banner: MerchandisingBanner | null;
784
+ active: {
785
+ from?: string;
786
+ until?: string;
787
+ } | null;
788
+ enabled: boolean;
789
+ status: "live" | "scheduled" | "ended" | "off" | "none";
790
+ };
791
+ /** What to change: each field given replaces the current one; null clears the banner or the times. */
792
+ export type ArrangementChange = {
793
+ order?: string[];
794
+ hidden?: string[];
795
+ banner?: MerchandisingBanner | null;
796
+ active?: {
797
+ from?: string;
798
+ until?: string;
799
+ } | null;
800
+ enabled?: boolean;
801
+ };
802
+ /** An image uploaded for the index's banners, hosted by Findlane, and the rules showing it. */
803
+ export type UploadedImage = {
804
+ key: string;
805
+ url: string;
806
+ bytes: number;
807
+ contentType: string;
808
+ width: number | null;
809
+ height: number | null;
810
+ createdAt: string;
811
+ usedBy: string[];
812
+ };
813
+ export type ImageLibrary = {
814
+ images: UploadedImage[];
815
+ used: {
816
+ files: number;
817
+ bytes: number;
818
+ };
819
+ limits: {
820
+ fileBytes: number;
821
+ files: number;
822
+ bytes: number;
823
+ };
824
+ };
825
+ /** Each banner's shown and clicks by its rule; `analytics` says whether they could be read. */
826
+ export type BannerStatsReport = {
827
+ days: number;
828
+ banners: {
829
+ rule: string;
830
+ shown: number;
831
+ clicks: number;
832
+ }[];
833
+ analytics: "ok" | "off" | "unavailable";
748
834
  };
749
835
  /** A past order for importOrders; `timestamp` is when it was placed (ms, a Date, or an ISO string). */
750
836
  export type Order = {
@@ -1039,6 +1125,39 @@ export declare class SearchServerClient {
1039
1125
  /** The index's lists: rows of products, such as Popular, and facet fields' values for menus, built ahead
1040
1126
  * and served from a cache, so reading them isn't counted as searches. Reading needs the `search` scope;
1041
1127
  * setting them up needs `write`. */
1128
+ /** Merchandising (0.23.0): category pages and searches arranged as the dashboard's Merchandising page does:
1129
+ * the products placed first, hidden products, a banner, and when. Saved as the page's rule within seconds,
1130
+ * keeping anything else the rule does. Reading needs the `search` or `write` scope; changing, `write`. */
1131
+ merchandising: {
1132
+ /** Every arranged category page and search. */
1133
+ list: (signal?: AbortSignal) => Promise<Arrangement[]>;
1134
+ /** One page's or search's arrangement; status "none" when nothing is arranged. */
1135
+ get: (target: MerchandisingTarget, signal?: AbortSignal) => Promise<Arrangement>;
1136
+ /** Changes one: `order` up to 20 objectIDs placed first, `hidden` up to 100, `banner` ({ image, link?, title?,
1137
+ * position? } or null), `active` ({ from?, until? } or null), `enabled`. Fields left out stay as they are. */
1138
+ set: (target: MerchandisingTarget, change: ArrangementChange, signal?: AbortSignal) => Promise<Arrangement>;
1139
+ /** Stops arranging one: its order, hidden products and banner go; the rule's other actions stay. */
1140
+ remove: (target: MerchandisingTarget, signal?: AbortSignal) => Promise<Arrangement>;
1141
+ };
1142
+ /** Images for the index's banners, hosted by Findlane at https://images.findlane.dev (0.23.0): JPEG, PNG,
1143
+ * WebP or AVIF, up to 2 MB each, 50 images and 100 MB an index. An image no rule shows is deleted a week
1144
+ * later. Uploading and deleting need the `write` scope. */
1145
+ images: {
1146
+ list: (signal?: AbortSignal) => Promise<ImageLibrary>;
1147
+ /** Uploads an image's bytes; the same image again gives the same URL. */
1148
+ upload: (image: Blob | ArrayBuffer | Uint8Array, options?: {
1149
+ contentType?: string;
1150
+ signal?: AbortSignal;
1151
+ }) => Promise<UploadedImage & {
1152
+ error?: string;
1153
+ }>;
1154
+ /** Uploads the image at an https address, which Findlane fetches. */
1155
+ uploadFromUrl: (url: string, signal?: AbortSignal) => Promise<UploadedImage>;
1156
+ /** Deletes an image by its key; refused (409) while a rule's banner shows it. */
1157
+ delete: (key: string, signal?: AbortSignal) => Promise<{
1158
+ deleted: true;
1159
+ }>;
1160
+ };
1042
1161
  lists: {
1043
1162
  /** One list by its ID; throws a FindlaneError with status 404 when there's none. `limit` shows fewer. */
1044
1163
  get: <R = T>(id: string, options?: {
@@ -1127,6 +1246,12 @@ export declare class SearchServerClient {
1127
1246
  /** Revenue from search: orders and add-to-carts of products found through search, per query and per
1128
1247
  * product (0.20.0). */
1129
1248
  revenue: (options?: Omit<AnalyticsOptions, "source">) => Promise<RevenueReport>;
1249
+ /** Each banner's shown (shoppers' searches that showed it, cache answers included) and clicks, by its rule,
1250
+ * over the last 7, 30 (by default) or 90 days (0.23.0). */
1251
+ banners: (options?: {
1252
+ range?: "7d" | "30d" | "90d";
1253
+ signal?: AbortSignal;
1254
+ }) => Promise<BannerStatsReport>;
1130
1255
  };
1131
1256
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
1132
1257
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
package/dist/index.js CHANGED
@@ -219,6 +219,32 @@ class SearchServerClient {
219
219
  const { signal, visitor } = requestOptions(requestOptionsOrSignal);
220
220
  return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
221
221
  },
222
+ merchandising: {
223
+ list: async (signal) => (await request("/merchandising", "GET", undefined, signal)).arrangements,
224
+ get: (target, signal) => request(`/merchandising/arrangement?${new URLSearchParams(target)}`, "GET", undefined, signal),
225
+ set: (target, change, signal) => request("/merchandising/arrangement", "PUT", { ...target, ...change }, signal),
226
+ remove: (target, signal) => request(`/merchandising/arrangement?${new URLSearchParams(target)}`, "DELETE", undefined, signal)
227
+ },
228
+ images: {
229
+ list: (signal) => request("/images", "GET", undefined, signal),
230
+ upload: async (image, options = {}) => {
231
+ const response = await this.fetcher(`${this.baseUrl}${path}/images`, {
232
+ method: "POST",
233
+ signal: options.signal,
234
+ headers: {
235
+ authorization: `Bearer ${this.key}`,
236
+ "content-type": options.contentType ?? (image instanceof Blob && image.type ? image.type : "application/octet-stream")
237
+ },
238
+ body: image
239
+ });
240
+ const data = await response.json();
241
+ if (!response.ok)
242
+ throw new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
243
+ return data;
244
+ },
245
+ uploadFromUrl: (url, signal) => request("/images", "POST", { url }, signal),
246
+ delete: (key, signal) => request(`/images/${key}`, "DELETE", undefined, signal)
247
+ },
222
248
  lists: {
223
249
  get: (id, options = {}) => request(`/lists/${encodeURIComponent(id)}${options.limit ? `?limit=${options.limit}` : ""}`, "GET", undefined, options.signal),
224
250
  getMany: async (ids, options = {}) => {
@@ -273,7 +299,8 @@ class SearchServerClient {
273
299
  products: (options = {}) => report("products", options),
274
300
  productCounts: (options = {}) => report("product-counts", options),
275
301
  product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options),
276
- revenue: (options = {}) => report("revenue", options)
302
+ revenue: (options = {}) => report("revenue", options),
303
+ banners: (options = {}) => request(`/banners?range=${options.range ?? "30d"}`, "GET", undefined, options.signal)
277
304
  },
278
305
  upsert: (records, signal) => request("/records", "POST", { records }, signal),
279
306
  getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findlane/server",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.",
5
5
  "keywords": ["findlane", "search", "full-text search", "typo tolerance", "ecommerce search", "search api", "sdk"],
6
6
  "license": "MIT",