@adventurelabs/scout-core 1.4.57 → 1.4.59

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,5 @@
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
+ export { parseStorageFilePath } from "./storagePath";
10
4
  export declare function generateSignedUrl(filePath: string, expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<string | null>;
11
5
  export declare function generateSignedUrlsBatch(filePaths: string[], expiresIn?: number, supabaseClient?: SupabaseClient<Database>): Promise<(string | null)[]>;
@@ -1,64 +1,18 @@
1
- "use server";
2
1
  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) {
14
- return null;
15
- }
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
- const parts = cleaned.split("/");
28
- if (parts.length < 2) {
29
- return null;
30
- }
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, "");
38
- }
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
- */
2
+ import { SIGNED_URL_EXPIRATION_SECONDS } from "../constants/db";
3
+ import { parseStorageFilePath } from "./storagePath";
4
+ export { parseStorageFilePath } from "./storagePath";
46
5
  export async function generateSignedUrl(filePath, expiresIn = SIGNED_URL_EXPIRATION_SECONDS, supabaseClient) {
47
6
  try {
48
7
  const supabase = supabaseClient || (await newServerClient());
49
- const bucket_name = getBucketFromFilePath(filePath);
50
- if (!bucket_name) {
8
+ const parsed = parseStorageFilePath(filePath);
9
+ if (!parsed) {
51
10
  console.error("Invalid file path:", filePath);
52
11
  return null;
53
12
  }
54
- const formattedPath = getFormattedPath(filePath);
55
- if (!formattedPath) {
56
- console.error("Invalid formatted path:", formattedPath);
57
- return null;
58
- }
59
13
  const { data, error } = await supabase.storage
60
- .from(bucket_name)
61
- .createSignedUrl(formattedPath, expiresIn);
14
+ .from(parsed.bucket)
15
+ .createSignedUrl(parsed.objectPath, expiresIn);
62
16
  if (error) {
63
17
  console.error("Error generating signed URL:", error.message);
64
18
  return null;
@@ -75,19 +29,14 @@ export async function generateSignedUrlsBatch(filePaths, expiresIn = SIGNED_URL_
75
29
  const supabase = supabaseClient || (await newServerClient());
76
30
  const signedUrlPromises = filePaths.map(async (filePath) => {
77
31
  try {
78
- const bucket_name = getBucketFromFilePath(filePath);
79
- if (!bucket_name) {
32
+ const parsed = parseStorageFilePath(filePath);
33
+ if (!parsed) {
80
34
  console.error("Invalid file path:", filePath);
81
35
  return null;
82
36
  }
83
- const formattedPath = getFormattedPath(filePath);
84
- if (!formattedPath) {
85
- console.error("Invalid formatted path:", formattedPath);
86
- return null;
87
- }
88
37
  const { data, error } = await supabase.storage
89
- .from(bucket_name)
90
- .createSignedUrl(formattedPath, expiresIn);
38
+ .from(parsed.bucket)
39
+ .createSignedUrl(parsed.objectPath, expiresIn);
91
40
  if (error) {
92
41
  console.warn(`Error generating signed URL for ${filePath}:`, error.message);
93
42
  return null;
@@ -0,0 +1,5 @@
1
+ /** Assumes DB `file_path` is always `bucket/object/...` */
2
+ export declare function parseStorageFilePath(filePath: string): {
3
+ bucket: string;
4
+ objectPath: string;
5
+ } | null;
@@ -0,0 +1,16 @@
1
+ /** Assumes DB `file_path` is always `bucket/object/...` */
2
+ export function parseStorageFilePath(filePath) {
3
+ const cleaned = filePath.trim().replace(/^\/+|\/+$/g, "");
4
+ if (!cleaned) {
5
+ return null;
6
+ }
7
+ const parts = cleaned.split("/");
8
+ if (parts.length < 2) {
9
+ return null;
10
+ }
11
+ const bucket = parts[0];
12
+ if (/^[0-9]+$/.test(bucket)) {
13
+ return null;
14
+ }
15
+ return { bucket, objectPath: parts.slice(1).join("/") };
16
+ }
@@ -2772,6 +2772,13 @@ export declare function useSupabase(): SupabaseClient<Database, "public", "publi
2772
2772
  status: string;
2773
2773
  }[];
2774
2774
  };
2775
+ transfer_device_to_herd: {
2776
+ Args: {
2777
+ p_device_id: number;
2778
+ p_target_herd_id: number;
2779
+ };
2780
+ Returns: undefined;
2781
+ };
2775
2782
  };
2776
2783
  Enums: {
2777
2784
  analysis_work_status: "waiting" | "cancelled" | "processing" | "failed" | "success";
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;
@@ -478,26 +504,12 @@ export const scoutApi = createApi({
478
504
  // Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
479
505
  const hasMore = rows.length >= limit;
480
506
  const resultRows = hasMore ? rows.slice(0, limit) : rows;
481
- const uniqueFilePaths = Array.from(new Set(resultRows.flatMap((row) => {
482
- const paths = [];
483
- if (row.event_data?.file_path)
484
- paths.push(row.event_data.file_path);
485
- if (row.artifact_data?.file_path)
486
- paths.push(row.artifact_data.file_path);
487
- return paths;
488
- })));
489
507
  let urlMap = new Map();
490
- if (uniqueFilePaths.length > 0) {
491
- try {
492
- const urlResults = await generateSignedUrlsBatch(uniqueFilePaths);
493
- urlResults.forEach((url, index) => {
494
- if (url)
495
- urlMap.set(uniqueFilePaths[index], url);
496
- });
497
- }
498
- catch (urlError) {
499
- console.warn("Failed to generate signed URLs for feed:", urlError);
500
- }
508
+ try {
509
+ urlMap = await signedUrlMapForFeedRows(resultRows);
510
+ }
511
+ catch (urlError) {
512
+ console.warn("Failed to generate signed URLs for feed:", urlError);
501
513
  }
502
514
  const items = resultRows.map((row) => ({
503
515
  ...row,
@@ -567,26 +579,12 @@ export const scoutApi = createApi({
567
579
  // Full page (rows.length >= limit) means there might be more; only signal hasMore when we have a nextCursor
568
580
  const hasMore = rows.length >= limit;
569
581
  const resultRows = hasMore ? rows.slice(0, limit) : rows;
570
- const uniqueFilePaths = Array.from(new Set(resultRows.flatMap((row) => {
571
- const paths = [];
572
- if (row.event_data?.file_path)
573
- paths.push(row.event_data.file_path);
574
- if (row.artifact_data?.file_path)
575
- paths.push(row.artifact_data.file_path);
576
- return paths;
577
- })));
578
582
  let urlMap = new Map();
579
- if (uniqueFilePaths.length > 0) {
580
- try {
581
- const urlResults = await generateSignedUrlsBatch(uniqueFilePaths);
582
- urlResults.forEach((url, index) => {
583
- if (url)
584
- urlMap.set(uniqueFilePaths[index], url);
585
- });
586
- }
587
- catch (urlError) {
588
- console.warn("Failed to generate signed URLs for feed:", urlError);
589
- }
583
+ try {
584
+ urlMap = await signedUrlMapForFeedRows(resultRows);
585
+ }
586
+ catch (urlError) {
587
+ console.warn("Failed to generate signed URLs for feed:", urlError);
590
588
  }
591
589
  const items = resultRows.map((row) => ({
592
590
  ...row,
@@ -2857,6 +2857,13 @@ export type Database = {
2857
2857
  status: string;
2858
2858
  }[];
2859
2859
  };
2860
+ transfer_device_to_herd: {
2861
+ Args: {
2862
+ p_device_id: number;
2863
+ p_target_herd_id: number;
2864
+ };
2865
+ Returns: undefined;
2866
+ };
2860
2867
  };
2861
2868
  Enums: {
2862
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.57",
3
+ "version": "1.4.59",
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",