@milaboratories/pl-drivers 1.14.15 → 1.15.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.
Files changed (36) hide show
  1. package/dist/clients/ls_api.cjs +1 -0
  2. package/dist/clients/ls_api.cjs.map +1 -1
  3. package/dist/clients/ls_api.js +1 -0
  4. package/dist/clients/ls_api.js.map +1 -1
  5. package/dist/drivers/helpers/ls_remote_import_handle.cjs +5 -3
  6. package/dist/drivers/helpers/ls_remote_import_handle.cjs.map +1 -1
  7. package/dist/drivers/helpers/ls_remote_import_handle.js +5 -3
  8. package/dist/drivers/helpers/ls_remote_import_handle.js.map +1 -1
  9. package/dist/drivers/ls.cjs +3 -3
  10. package/dist/drivers/ls.cjs.map +1 -1
  11. package/dist/drivers/ls.d.ts +6 -6
  12. package/dist/drivers/ls.d.ts.map +1 -1
  13. package/dist/drivers/ls.js +3 -3
  14. package/dist/drivers/ls.js.map +1 -1
  15. package/dist/drivers/types.cjs +2 -1
  16. package/dist/drivers/types.cjs.map +1 -1
  17. package/dist/drivers/types.d.ts +14 -0
  18. package/dist/drivers/types.d.ts.map +1 -1
  19. package/dist/drivers/types.js +2 -1
  20. package/dist/drivers/types.js.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.cjs +31 -0
  23. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.cjs.map +1 -1
  24. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.d.ts +11 -0
  25. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.d.ts.map +1 -1
  26. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.js +31 -0
  27. package/dist/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.js.map +1 -1
  28. package/package.json +5 -5
  29. package/src/clients/ls_api.ts +1 -0
  30. package/src/drivers/helpers/ls_remote_import_handle.test.ts +46 -0
  31. package/src/drivers/helpers/ls_remote_import_handle.ts +6 -0
  32. package/src/drivers/ls.test.ts +138 -2
  33. package/src/drivers/ls.ts +9 -9
  34. package/src/drivers/types.ts +5 -0
  35. package/src/proto-grpc/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.ts +34 -0
  36. package/src/proto-rest/lsapi.ts +8 -0
@@ -1,10 +1,13 @@
1
1
  import { ConsoleLoggerAdapter, HmacSha256Signer } from "@milaboratories/ts-helpers";
2
- import { LsDriver } from "./ls";
2
+ import { LsDriver, type LsEntryWithFileStats } from "./ls";
3
3
  import { TestHelpers } from "@milaboratories/pl-client";
4
4
  import * as path from "node:path";
5
- import { test, expect } from "vitest";
5
+ import { test, expect, describe } from "vitest";
6
6
  import { isImportFileHandleIndex, isImportFileHandleUpload } from "@milaboratories/pl-model-common";
7
+ import type { StorageHandle } from "@milaboratories/pl-model-common";
7
8
  import * as env from "../test_env";
9
+ import { parseIndexHandle } from "./helpers/ls_remote_import_handle";
10
+ import { createRemoteStorageHandle } from "./helpers/ls_storage_entry";
8
11
 
9
12
  const assetsPath = path.resolve("../../../assets");
10
13
 
@@ -141,3 +144,136 @@ test("should ok when get file using local dialog, and read its content", async (
141
144
  expect(multiResult.files![0]).toStrictEqual(result.file);
142
145
  });
143
146
  });
147
+
148
+ // Unit tests: verify that LsDriver.listFiles and listRemoteFilesWithFileStats correctly
149
+ // thread additionalInfo from gRPC list items into the index:// handle.
150
+ describe("LsDriver additionalInfo threading", () => {
151
+ const envelope = { uid: "u1", sid: "s1", sig: "sigval", exp: "9999999999", kid: "k1", v: "1" };
152
+
153
+ const storageInfo = {
154
+ storageId: "test-storage",
155
+ storageName: "Test Storage",
156
+ // Signed resource id format "<globalId>|<signatureHex>" — required by asSignedResourceId.
157
+ resourceId: "res-id|deadbeef" as any,
158
+ resourceType: { name: "LS/test-storage", version: "1" },
159
+ };
160
+
161
+ // Builds a minimal LsDriver instance with an injected mock lsClient via private-constructor bypass.
162
+ function makeMockDriver(listResponse: { items: any[]; delimiter: string }): LsDriver {
163
+ const mockLsClient = {
164
+ list: async () => listResponse,
165
+ close: () => {},
166
+ };
167
+ const mockUserResources = {
168
+ getDataLibraries: async () => new Map([[storageInfo.storageId, storageInfo]]),
169
+ };
170
+ const signer = new HmacSha256Signer("test");
171
+ // Bypass private constructor for unit testing only.
172
+ return new (LsDriver as any)(
173
+ new ConsoleLoggerAdapter(),
174
+ mockLsClient,
175
+ mockUserResources,
176
+ signer,
177
+ new Map(),
178
+ new Map(),
179
+ () => Promise.resolve(undefined),
180
+ ) as LsDriver;
181
+ }
182
+
183
+ function makeRemoteHandle(): StorageHandle {
184
+ return createRemoteStorageHandle(storageInfo) as StorageHandle;
185
+ }
186
+
187
+ test("listFiles: handle carries additionalInfo envelope from gRPC item", async () => {
188
+ const driver = makeMockDriver({
189
+ delimiter: "/",
190
+ items: [
191
+ {
192
+ name: "file.txt",
193
+ size: 100n,
194
+ isDir: false,
195
+ additionalInfo: envelope,
196
+ fullName: "dir/file.txt",
197
+ directory: "dir/",
198
+ version: "v1",
199
+ },
200
+ ],
201
+ });
202
+
203
+ const result = await driver.listFiles(makeRemoteHandle(), "dir/");
204
+ expect(result.entries).toHaveLength(1);
205
+
206
+ const parsed = parseIndexHandle(result.entries[0].handle as any);
207
+ expect(parsed.additionalInfo).toEqual(envelope);
208
+ });
209
+
210
+ test("listFiles: handle has no additionalInfo when item has empty envelope", async () => {
211
+ const driver = makeMockDriver({
212
+ delimiter: "/",
213
+ items: [
214
+ {
215
+ name: "plain.txt",
216
+ size: 50n,
217
+ isDir: false,
218
+ additionalInfo: {},
219
+ fullName: "plain.txt",
220
+ directory: "",
221
+ version: "v1",
222
+ },
223
+ ],
224
+ });
225
+
226
+ const result = await driver.listFiles(makeRemoteHandle(), "");
227
+ expect(result.entries).toHaveLength(1);
228
+
229
+ const parsed = parseIndexHandle(result.entries[0].handle as any);
230
+ expect(parsed.additionalInfo).toBeUndefined();
231
+ });
232
+
233
+ test("listRemoteFilesWithFileStats: handle carries additionalInfo envelope", async () => {
234
+ const driver = makeMockDriver({
235
+ delimiter: "/",
236
+ items: [
237
+ {
238
+ name: "data.csv",
239
+ size: 200n,
240
+ isDir: false,
241
+ additionalInfo: envelope,
242
+ fullName: "data.csv",
243
+ directory: "",
244
+ version: "v2",
245
+ },
246
+ ],
247
+ });
248
+
249
+ const result = await driver.listRemoteFilesWithFileStats(makeRemoteHandle(), "");
250
+ expect(result.entries).toHaveLength(1);
251
+ expect((result.entries[0] as LsEntryWithFileStats).size).toBe(200);
252
+
253
+ const parsed = parseIndexHandle(result.entries[0].handle as any);
254
+ expect(parsed.additionalInfo).toEqual(envelope);
255
+ });
256
+
257
+ test("listRemoteFilesWithFileStats: no additionalInfo when absent in item", async () => {
258
+ const driver = makeMockDriver({
259
+ delimiter: "/",
260
+ items: [
261
+ {
262
+ name: "plain.csv",
263
+ size: 10n,
264
+ isDir: false,
265
+ additionalInfo: {},
266
+ fullName: "plain.csv",
267
+ directory: "",
268
+ version: "v1",
269
+ },
270
+ ],
271
+ });
272
+
273
+ const result = await driver.listRemoteFilesWithFileStats(makeRemoteHandle(), "");
274
+ expect(result.entries).toHaveLength(1);
275
+
276
+ const parsed = parseIndexHandle(result.entries[0].handle as any);
277
+ expect(parsed.additionalInfo).toBeUndefined();
278
+ });
279
+ });
package/src/drivers/ls.ts CHANGED
@@ -34,18 +34,18 @@ export interface InternalLsDriver extends sdk.LsDriver {
34
34
  * */
35
35
  getLocalFileHandle(localPath: string): Promise<sdk.LocalImportFileHandle>;
36
36
 
37
- listRemoteFilesWithAdditionalInfo(
37
+ listRemoteFilesWithFileStats(
38
38
  storage: sdk.StorageHandle,
39
39
  fullPath: string,
40
- ): Promise<ListRemoteFilesResultWithAdditionalInfo>;
40
+ ): Promise<ListRemoteFilesResultWithFileStats>;
41
41
  }
42
42
 
43
- export type ListRemoteFilesResultWithAdditionalInfo = {
43
+ export type ListRemoteFilesResultWithFileStats = {
44
44
  parent?: string;
45
- entries: LsEntryWithAdditionalInfo[];
45
+ entries: LsEntryWithFileStats[];
46
46
  };
47
47
 
48
- export type LsEntryWithAdditionalInfo = sdk.LsEntry & {
48
+ export type LsEntryWithFileStats = sdk.LsEntry & {
49
49
  size: number;
50
50
  };
51
51
 
@@ -217,7 +217,7 @@ export class LsDriver implements InternalLsDriver {
217
217
  type: e.isDir ? "dir" : "file",
218
218
  name: e.name,
219
219
  fullPath: e.fullName,
220
- handle: createIndexImportHandle(storageData.storageId, e.fullName),
220
+ handle: createIndexImportHandle(storageData.storageId, e.fullName, e.additionalInfo),
221
221
  })),
222
222
  };
223
223
  }
@@ -249,10 +249,10 @@ export class LsDriver implements InternalLsDriver {
249
249
  return { entries };
250
250
  }
251
251
 
252
- public async listRemoteFilesWithAdditionalInfo(
252
+ public async listRemoteFilesWithFileStats(
253
253
  storageHandle: sdk.StorageHandle,
254
254
  fullPath: string,
255
- ): Promise<ListRemoteFilesResultWithAdditionalInfo> {
255
+ ): Promise<ListRemoteFilesResultWithFileStats> {
256
256
  const storageData = parseStorageHandle(storageHandle);
257
257
  if (!storageData.isRemote) {
258
258
  throw new Error(`Storage ${storageData.name} is not remote`);
@@ -266,7 +266,7 @@ export class LsDriver implements InternalLsDriver {
266
266
  type: e.isDir ? "dir" : "file",
267
267
  name: e.name,
268
268
  fullPath: e.fullName,
269
- handle: createIndexImportHandle(storageData.storageId, e.fullName),
269
+ handle: createIndexImportHandle(storageData.storageId, e.fullName, e.additionalInfo),
270
270
  size: Number(e.size),
271
271
  })),
272
272
  };
@@ -54,6 +54,11 @@ export const ImportFileHandleIndexData = z.object({
54
54
  storageId: z.string(),
55
55
  /** Path inside storage */
56
56
  path: z.string(),
57
+ /**
58
+ * Federative identity envelope from LsAPI.List.Response.ListItem.additional_info.
59
+ * Absent for non-federative storages (backwards compatible: old handles parse unchanged).
60
+ */
61
+ additionalInfo: z.record(z.string(), z.string()).optional(),
57
62
  });
58
63
  export type ImportFileHandleIndexData = z.infer<typeof ImportFileHandleIndexData>;
59
64
 
@@ -40,6 +40,16 @@ export interface LsAPI_ListItem {
40
40
  * @generated from protobuf field: bool is_dir = 3
41
41
  */
42
42
  isDir: boolean;
43
+ /**
44
+ * additional_info carries the signed identity envelope for federative storages.
45
+ * KV schema: v=1, path, uid, sid, role, exp, kid, signed, sig.
46
+ * Empty for non-federative storages. Verifiers MUST ignore unknown keys.
47
+ *
48
+ * @generated from protobuf field: map<string, string> additional_info = 8
49
+ */
50
+ additionalInfo: {
51
+ [key: string]: string;
52
+ };
43
53
  /**
44
54
  * full_name is the full name of the item, relative to the storage root.
45
55
  * It is <directory> + <name>.
@@ -169,6 +179,7 @@ class LsAPI_ListItem$Type extends MessageType<LsAPI_ListItem> {
169
179
  { no: 1, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
170
180
  { no: 2, name: "size", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
171
181
  { no: 3, name: "is_dir", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
182
+ { no: 8, name: "additional_info", kind: "map", K: 9 /*ScalarType.STRING*/, V: { kind: "scalar", T: 9 /*ScalarType.STRING*/ } },
172
183
  { no: 10, name: "full_name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
173
184
  { no: 11, name: "directory", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
174
185
  { no: 12, name: "last_modified", kind: "message", T: () => Timestamp },
@@ -180,6 +191,7 @@ class LsAPI_ListItem$Type extends MessageType<LsAPI_ListItem> {
180
191
  message.name = "";
181
192
  message.size = 0n;
182
193
  message.isDir = false;
194
+ message.additionalInfo = {};
183
195
  message.fullName = "";
184
196
  message.directory = "";
185
197
  message.version = "";
@@ -201,6 +213,9 @@ class LsAPI_ListItem$Type extends MessageType<LsAPI_ListItem> {
201
213
  case /* bool is_dir */ 3:
202
214
  message.isDir = reader.bool();
203
215
  break;
216
+ case /* map<string, string> additional_info */ 8:
217
+ this.binaryReadMap8(message.additionalInfo, reader, options);
218
+ break;
204
219
  case /* string full_name */ 10:
205
220
  message.fullName = reader.string();
206
221
  break;
@@ -224,6 +239,22 @@ class LsAPI_ListItem$Type extends MessageType<LsAPI_ListItem> {
224
239
  }
225
240
  return message;
226
241
  }
242
+ private binaryReadMap8(map: LsAPI_ListItem["additionalInfo"], reader: IBinaryReader, options: BinaryReadOptions): void {
243
+ let len = reader.uint32(), end = reader.pos + len, key: keyof LsAPI_ListItem["additionalInfo"] | undefined, val: LsAPI_ListItem["additionalInfo"][any] | undefined;
244
+ while (reader.pos < end) {
245
+ let [fieldNo, wireType] = reader.tag();
246
+ switch (fieldNo) {
247
+ case 1:
248
+ key = reader.string();
249
+ break;
250
+ case 2:
251
+ val = reader.string();
252
+ break;
253
+ default: throw new globalThis.Error("unknown map entry field for MiLaboratories.Controller.Shared.LsAPI.ListItem.additional_info");
254
+ }
255
+ }
256
+ map[key ?? ""] = val ?? "";
257
+ }
227
258
  internalBinaryWrite(message: LsAPI_ListItem, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
228
259
  /* string name = 1; */
229
260
  if (message.name !== "")
@@ -234,6 +265,9 @@ class LsAPI_ListItem$Type extends MessageType<LsAPI_ListItem> {
234
265
  /* bool is_dir = 3; */
235
266
  if (message.isDir !== false)
236
267
  writer.tag(3, WireType.Varint).bool(message.isDir);
268
+ /* map<string, string> additional_info = 8; */
269
+ for (let k of globalThis.Object.keys(message.additionalInfo))
270
+ writer.tag(8, WireType.LengthDelimited).fork().tag(1, WireType.LengthDelimited).string(k).tag(2, WireType.LengthDelimited).string(message.additionalInfo[k]).join();
237
271
  /* string full_name = 10; */
238
272
  if (message.fullName !== "")
239
273
  writer.tag(10, WireType.LengthDelimited).string(message.fullName);
@@ -41,6 +41,14 @@ export interface components {
41
41
  size: string;
42
42
  /** @description is_dir is true for an item that can have subitems. */
43
43
  isDir: boolean;
44
+ /**
45
+ * @description additional_info carries the signed identity envelope for federative storages.
46
+ * KV schema: v=1, path, uid, sid, role, exp, kid, signed, sig.
47
+ * Empty for non-federative storages. Verifiers MUST ignore unknown keys.
48
+ */
49
+ additionalInfo?: {
50
+ [key: string]: string;
51
+ };
44
52
  /**
45
53
  * @description full_name is the full name of the item, relative to the storage root.
46
54
  * It is <directory> + <name>.