@adventurelabs/scout-core 1.4.56 → 1.4.58

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.
@@ -1,2 +1,4 @@
1
1
  export declare const BUCKET_NAME_SCOUT = "scout";
2
+ /** Event media when `file_path` uses the `events/...` prefix */
3
+ export declare const BUCKET_NAME_EVENTS = "events";
2
4
  export declare const SIGNED_URL_EXPIRATION_SECONDS: number;
@@ -1,3 +1,4 @@
1
1
  export const BUCKET_NAME_SCOUT = "scout";
2
- // Signed URL expiration time in seconds (12 hours = 43200 seconds)
2
+ /** Event media when `file_path` uses the `events/...` prefix */
3
+ export const BUCKET_NAME_EVENTS = "events";
3
4
  export const SIGNED_URL_EXPIRATION_SECONDS = 12 * 60 * 60;
@@ -0,0 +1,2 @@
1
+ import { IWebResponseCompatible } from "../types/requests";
2
+ export declare function transferDeviceToHerd(deviceId: number, targetHerdId: number): Promise<IWebResponseCompatible<boolean>>;
@@ -0,0 +1,14 @@
1
+ "use server";
2
+ import { newServerClient } from "../supabase/server";
3
+ import { IWebResponse } from "../types/requests";
4
+ export async function transferDeviceToHerd(deviceId, targetHerdId) {
5
+ const client = await newServerClient();
6
+ const { error } = await client.rpc("transfer_device_to_herd", {
7
+ p_device_id: deviceId,
8
+ p_target_herd_id: targetHerdId,
9
+ });
10
+ if (error) {
11
+ return IWebResponse.error(error.message).to_compatible();
12
+ }
13
+ return IWebResponse.success(true).to_compatible();
14
+ }
@@ -1,11 +1,9 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
- /**
4
- * Generates a signed URL for a file in Supabase storage
5
- * @param filePath - The path to the file in storage (e.g., "events/123/image.jpg")
6
- * @param expiresIn - Number of seconds until the URL expires (default: 12 hours)
7
- * @param supabaseClient - Optional Supabase client (will create new one if not provided)
8
- * @returns Promise<string | null> - The signed URL or null if error
9
- */
3
+ /** DB `file_path` is always `bucket/object/...` (e.g. `events/254/...`, `artifacts/254/...`). */
4
+ export declare function parseStorageFilePath(filePath: string): {
5
+ bucket: string;
6
+ objectPath: string;
7
+ } | null;
10
8
  export declare function generateSignedUrl(filePath: string, expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<string | null>;
11
9
  export declare function generateSignedUrlsBatch(filePaths: string[], expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<(string | null)[]>;
@@ -1,64 +1,33 @@
1
1
  "use server";
2
2
  import { newServerClient } from "../supabase/server";
3
- import { SIGNED_URL_EXPIRATION_SECONDS, } from "../constants/db";
4
- /**
5
- * Splits file path into bucket name and path. Must be at leaast one slash in your file path
6
- * @param filePath
7
- * @returns IFilePathParts | null - null if invalid file path
8
- */
9
- function getBucketFromFilePath(filePath) {
10
- // delete any start or end slashes/whitespace
11
- filePath = filePath.replace(/^\/+|\/+$/g, "");
12
- const parts = filePath.split("/");
13
- if (parts.length < 2) {
3
+ import { SIGNED_URL_EXPIRATION_SECONDS } from "../constants/db";
4
+ /** DB `file_path` is always `bucket/object/...` (e.g. `events/254/...`, `artifacts/254/...`). */
5
+ export function parseStorageFilePath(filePath) {
6
+ const cleaned = filePath.trim().replace(/^\/+|\/+$/g, "");
7
+ if (!cleaned) {
14
8
  return null;
15
9
  }
16
- const bucket_name = parts[0];
17
- return bucket_name;
18
- }
19
- /**
20
- * Extracts the short path from a file path
21
- * @param filePath
22
- * @returns string | null - null if invalid file path
23
- */
24
- // for example if the input is /artifacts/10/52/test.mp4 - the output shld be 10/52/test.mp4
25
- function getFormattedPath(filePath) {
26
- const cleaned = cleanPath(filePath);
27
10
  const parts = cleaned.split("/");
28
11
  if (parts.length < 2) {
29
12
  return null;
30
13
  }
31
- // Remove the first part (bucket name) and return the rest
32
- return parts.slice(1).join("/");
33
- }
34
- /** Removes leading and trailing slashes and leading/trailing whitespace */
35
- function cleanPath(filePath) {
36
- // delete leading/trailing slash and whitespace
37
- return filePath.trim().replace(/^\/+|\/+$/g, "");
14
+ const bucket = parts[0];
15
+ if (/^[0-9]+$/.test(bucket)) {
16
+ return null;
17
+ }
18
+ return { bucket, objectPath: parts.slice(1).join("/") };
38
19
  }
39
- /**
40
- * Generates a signed URL for a file in Supabase storage
41
- * @param filePath - The path to the file in storage (e.g., "events/123/image.jpg")
42
- * @param expiresIn - Number of seconds until the URL expires (default: 12 hours)
43
- * @param supabaseClient - Optional Supabase client (will create new one if not provided)
44
- * @returns Promise<string | null> - The signed URL or null if error
45
- */
46
20
  export async function generateSignedUrl(filePath, expiresIn = SIGNED_URL_EXPIRATION_SECONDS, supabaseClient) {
47
21
  try {
48
22
  const supabase = supabaseClient || (await newServerClient());
49
- const bucket_name = getBucketFromFilePath(filePath);
50
- if (!bucket_name) {
23
+ const parsed = parseStorageFilePath(filePath);
24
+ if (!parsed) {
51
25
  console.error("Invalid file path:", filePath);
52
26
  return null;
53
27
  }
54
- const formattedPath = getFormattedPath(filePath);
55
- if (!formattedPath) {
56
- console.error("Invalid formatted path:", formattedPath);
57
- return null;
58
- }
59
28
  const { data, error } = await supabase.storage
60
- .from(bucket_name)
61
- .createSignedUrl(formattedPath, expiresIn);
29
+ .from(parsed.bucket)
30
+ .createSignedUrl(parsed.objectPath, expiresIn);
62
31
  if (error) {
63
32
  console.error("Error generating signed URL:", error.message);
64
33
  return null;
@@ -75,19 +44,14 @@ export async function generateSignedUrlsBatch(filePaths, expiresIn = SIGNED_URL_
75
44
  const supabase = supabaseClient || (await newServerClient());
76
45
  const signedUrlPromises = filePaths.map(async (filePath) => {
77
46
  try {
78
- const bucket_name = getBucketFromFilePath(filePath);
79
- if (!bucket_name) {
47
+ const parsed = parseStorageFilePath(filePath);
48
+ if (!parsed) {
80
49
  console.error("Invalid file path:", filePath);
81
50
  return null;
82
51
  }
83
- const formattedPath = getFormattedPath(filePath);
84
- if (!formattedPath) {
85
- console.error("Invalid formatted path:", formattedPath);
86
- return null;
87
- }
88
52
  const { data, error } = await supabase.storage
89
- .from(bucket_name)
90
- .createSignedUrl(formattedPath, expiresIn);
53
+ .from(parsed.bucket)
54
+ .createSignedUrl(parsed.objectPath, expiresIn);
91
55
  if (error) {
92
56
  console.warn(`Error generating signed URL for ${filePath}:`, error.message);
93
57
  return null;
@@ -2488,6 +2488,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
2488
2488
  limit_caller?: number;
2489
2489
  min_flight_distance_meters?: number;
2490
2490
  min_flight_time_minutes?: number;
2491
+ product_numbers_caller?: string[];
2491
2492
  range_end?: string;
2492
2493
  range_start?: string;
2493
2494
  };
@@ -2507,6 +2508,7 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
2507
2508
  limit_caller?: number;
2508
2509
  min_flight_distance_meters?: number;
2509
2510
  min_flight_time_minutes?: number;
2511
+ product_numbers_caller?: string[];
2510
2512
  range_end?: string;
2511
2513
  range_start?: string;
2512
2514
  };
@@ -2770,6 +2772,13 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
2770
2772
  status: string;
2771
2773
  }[];
2772
2774
  };
2775
+ transfer_device_to_herd: {
2776
+ Args: {
2777
+ p_device_id: number;
2778
+ p_target_herd_id: number;
2779
+ };
2780
+ Returns: undefined;
2781
+ };
2773
2782
  };
2774
2783
  Enums: {
2775
2784
  analysis_work_status: "waiting" | "cancelled" | "processing" | "failed" | "success";
@@ -15,6 +15,11 @@ export interface InfiniteQueryArgs {
15
15
  minFlightTimeMinutes?: number | null;
16
16
  /** Sessions RPC only: minimum distance_total in meters. */
17
17
  minFlightDistanceMeters?: number | null;
18
+ /**
19
+ * Sessions infinite RPCs only: filter to devices with a matching non-deleted part.
20
+ * Omitted/null = no filter; empty array = no sessions.
21
+ */
22
+ productNumbers?: string[] | null;
18
23
  }
19
24
  export interface SessionsInfiniteResponse {
20
25
  sessions: ISessionWithCoordinates[];
package/dist/store/api.js CHANGED
@@ -1,5 +1,31 @@
1
1
  import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react";
2
2
  import { generateSignedUrlsBatch } from "../helpers/storage";
3
+ async function signedUrlMapForFeedRows(rows) {
4
+ const seen = new Set();
5
+ const paths = [];
6
+ for (const row of rows) {
7
+ const ep = row.event_data?.file_path;
8
+ if (ep && !seen.has(ep)) {
9
+ seen.add(ep);
10
+ paths.push(ep);
11
+ }
12
+ const ap = row.artifact_data?.file_path;
13
+ if (ap && !seen.has(ap)) {
14
+ seen.add(ap);
15
+ paths.push(ap);
16
+ }
17
+ }
18
+ const map = new Map();
19
+ if (paths.length === 0) {
20
+ return map;
21
+ }
22
+ const r = await generateSignedUrlsBatch(paths);
23
+ paths.forEach((p, i) => {
24
+ if (r[i])
25
+ map.set(p, r[i]);
26
+ });
27
+ return map;
28
+ }
3
29
  // Custom serialize function to exclude supabase client
4
30
  const serializeQueryArgs = ({ queryArgs, endpointDefinition, endpointName, }) => {
5
31
  const { supabase, ...serializableArgs } = queryArgs;
@@ -16,7 +42,7 @@ export const scoutApi = createApi({
16
42
  // =====================================================
17
43
  getSessionsInfiniteByHerd: builder.query({
18
44
  serializeQueryArgs,
19
- async queryFn({ herdId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, supabase, }) {
45
+ async queryFn({ herdId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, productNumbers, supabase, }) {
20
46
  try {
21
47
  if (!herdId) {
22
48
  return {
@@ -32,6 +58,7 @@ export const scoutApi = createApi({
32
58
  range_end: rangeEnd ?? null,
33
59
  min_flight_time_minutes: minFlightTimeMinutes ?? null,
34
60
  min_flight_distance_meters: minFlightDistanceMeters ?? null,
61
+ product_numbers_caller: productNumbers ?? undefined,
35
62
  });
36
63
  if (error) {
37
64
  return {
@@ -72,7 +99,7 @@ export const scoutApi = createApi({
72
99
  }),
73
100
  getSessionsInfiniteByDevice: builder.query({
74
101
  serializeQueryArgs,
75
- async queryFn({ deviceId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, supabase, }) {
102
+ async queryFn({ deviceId, limit = 20, cursor, rangeStart, rangeEnd, minFlightTimeMinutes, minFlightDistanceMeters, productNumbers, supabase, }) {
76
103
  try {
77
104
  if (!deviceId) {
78
105
  return {
@@ -88,6 +115,7 @@ export const scoutApi = createApi({
88
115
  range_end: rangeEnd ?? null,
89
116
  min_flight_time_minutes: minFlightTimeMinutes ?? null,
90
117
  min_flight_distance_meters: minFlightDistanceMeters ?? null,
118
+ product_numbers_caller: productNumbers ?? undefined,
91
119
  });
92
120
  if (error) {
93
121
  return {
@@ -476,26 +504,12 @@ export const scoutApi = createApi({
476
504
  // Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
477
505
  const hasMore = rows.length >= limit;
478
506
  const resultRows = hasMore ? rows.slice(0, limit) : rows;
479
- const uniqueFilePaths = Array.from(new Set(resultRows.flatMap((row) => {
480
- const paths = [];
481
- if (row.event_data?.file_path)
482
- paths.push(row.event_data.file_path);
483
- if (row.artifact_data?.file_path)
484
- paths.push(row.artifact_data.file_path);
485
- return paths;
486
- })));
487
507
  let urlMap = new Map();
488
- if (uniqueFilePaths.length > 0) {
489
- try {
490
- const urlResults = await generateSignedUrlsBatch(uniqueFilePaths);
491
- urlResults.forEach((url, index) => {
492
- if (url)
493
- urlMap.set(uniqueFilePaths[index], url);
494
- });
495
- }
496
- catch (urlError) {
497
- console.warn("Failed to generate signed URLs for feed:", urlError);
498
- }
508
+ try {
509
+ urlMap = await signedUrlMapForFeedRows(resultRows);
510
+ }
511
+ catch (urlError) {
512
+ console.warn("Failed to generate signed URLs for feed:", urlError);
499
513
  }
500
514
  const items = resultRows.map((row) => ({
501
515
  ...row,
@@ -565,26 +579,12 @@ export const scoutApi = createApi({
565
579
  // Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
566
580
  const hasMore = rows.length >= limit;
567
581
  const resultRows = hasMore ? rows.slice(0, limit) : rows;
568
- const uniqueFilePaths = Array.from(new Set(resultRows.flatMap((row) => {
569
- const paths = [];
570
- if (row.event_data?.file_path)
571
- paths.push(row.event_data.file_path);
572
- if (row.artifact_data?.file_path)
573
- paths.push(row.artifact_data.file_path);
574
- return paths;
575
- })));
576
582
  let urlMap = new Map();
577
- if (uniqueFilePaths.length > 0) {
578
- try {
579
- const urlResults = await generateSignedUrlsBatch(uniqueFilePaths);
580
- urlResults.forEach((url, index) => {
581
- if (url)
582
- urlMap.set(uniqueFilePaths[index], url);
583
- });
584
- }
585
- catch (urlError) {
586
- console.warn("Failed to generate signed URLs for feed:", urlError);
587
- }
583
+ try {
584
+ urlMap = await signedUrlMapForFeedRows(resultRows);
585
+ }
586
+ catch (urlError) {
587
+ console.warn("Failed to generate signed URLs for feed:", urlError);
588
588
  }
589
589
  const items = resultRows.map((row) => ({
590
590
  ...row,
@@ -2573,6 +2573,7 @@ export type Database = {
2573
2573
  limit_caller?: number;
2574
2574
  min_flight_distance_meters?: number;
2575
2575
  min_flight_time_minutes?: number;
2576
+ product_numbers_caller?: string[];
2576
2577
  range_end?: string;
2577
2578
  range_start?: string;
2578
2579
  };
@@ -2592,6 +2593,7 @@ export type Database = {
2592
2593
  limit_caller?: number;
2593
2594
  min_flight_distance_meters?: number;
2594
2595
  min_flight_time_minutes?: number;
2596
+ product_numbers_caller?: string[];
2595
2597
  range_end?: string;
2596
2598
  range_start?: string;
2597
2599
  };
@@ -2855,6 +2857,13 @@ export type Database = {
2855
2857
  status: string;
2856
2858
  }[];
2857
2859
  };
2860
+ transfer_device_to_herd: {
2861
+ Args: {
2862
+ p_device_id: number;
2863
+ p_target_herd_id: number;
2864
+ };
2865
+ Returns: undefined;
2866
+ };
2858
2867
  };
2859
2868
  Enums: {
2860
2869
  analysis_work_status: "waiting" | "cancelled" | "processing" | "failed" | "success";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.4.56",
3
+ "version": "1.4.58",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  "build": "tsc",
13
13
  "prepare": "yarn build",
14
14
  "dev": "tsc --watch",
15
- "clean": "rm -rf dist"
15
+ "clean": "rm -rf dist",
16
+ "migrate-storage-paths-device-prefix": "node scripts/migrate-storage-paths-device-prefix.mjs"
16
17
  },
17
18
  "keywords": [
18
19
  "scout",