@voltro/plugin-storage 0.84.0 → 0.86.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.
@@ -0,0 +1,493 @@
1
+ import { canonicalStorageRoot as e, filesystemObjectNamespace as t, storageEndpointIdentity as n, storageObjectNamespace as r } from "./storageNamespaces.js";
2
+ import { STORAGE_BLOBS_TABLE as i } from "./table.js";
3
+ import { StorageError as a } from "./types.js";
4
+ import { storageInteger as o } from "./storageNumbers.js";
5
+ import { concatBytes as s, fromNodeReadable as c, toNodeReadable as l } from "./streamBridge.js";
6
+ import { storageEnv as u } from "./env.js";
7
+ import { storageMetadataError as d } from "./metadataError.js";
8
+ import { randomUUID as f } from "node:crypto";
9
+ import { authoredError as p, generateId as m } from "@voltro/database";
10
+ import { Effect as h } from "effect";
11
+ //#region src/providers.ts
12
+ var g = (e, t, n, r) => new a({
13
+ provider: e,
14
+ message: t,
15
+ transient: n,
16
+ ...r === void 0 ? {} : { status: r }
17
+ }), _ = (e) => e === void 0 || e === 408 || e === 429 || e >= 500, v = f(), y = /* @__PURE__ */ new Map(), b = (e, t) => y.get(e)?.get(t)?.bytes, x = (e) => {
18
+ y.get(e)?.clear();
19
+ }, S = (e = "default") => {
20
+ let t = y.get(e) ?? /* @__PURE__ */ new Map();
21
+ return y.set(e, t), {
22
+ name: "memory",
23
+ objectNamespace: h.succeed(r([
24
+ "memory",
25
+ v,
26
+ e
27
+ ])),
28
+ presigns: !1,
29
+ put: (e, n, r) => h.sync(() => (t.set(e, {
30
+ bytes: n,
31
+ contentType: r.contentType
32
+ }), { size: n.byteLength })),
33
+ get: (e) => h.suspend(() => {
34
+ let n = t.get(e);
35
+ return n ? h.succeed({
36
+ bytes: n.bytes,
37
+ contentType: n.contentType
38
+ }) : h.fail(g("memory", `no object at key '${e}'`, !1, 404));
39
+ }),
40
+ delete: (e) => h.sync(() => {
41
+ t.delete(e);
42
+ }),
43
+ publicUrl: () => null,
44
+ getUrl: () => h.succeed(""),
45
+ head: (e) => h.sync(() => {
46
+ let n = t.get(e);
47
+ return n ? { size: n.bytes.byteLength } : null;
48
+ })
49
+ };
50
+ }, C = (n) => {
51
+ let r = e(n.root), i = async (e) => {
52
+ if (e.split("/")[0]?.startsWith(".voltro-object-namespace")) throw Error("Storage namespace metadata is not an object key");
53
+ let { join: t, resolve: n, sep: i } = await import("node:path"), a = n(r), o = n(a, e);
54
+ if (o.startsWith(n(a, ".voltro-object-namespace"))) throw Error("Storage namespace metadata is not an object key");
55
+ if (o !== a && !o.startsWith(a + i)) throw Error(`key '${e}' escapes the storage root`);
56
+ return t(o);
57
+ }, a = (e, t, n = !1) => h.tryPromise({
58
+ try: e,
59
+ catch: (e) => e?.code === "ENOENT" ? g("filesystem", `no object at key '${t}'`, !1, 404) : g("filesystem", String(e?.message ?? e), n)
60
+ });
61
+ return {
62
+ root: r,
63
+ name: "filesystem",
64
+ objectNamespace: h.try({
65
+ try: () => t(r),
66
+ catch: (e) => g("filesystem", String(e instanceof Error ? e.message : e), !1)
67
+ }),
68
+ presigns: !1,
69
+ put: (e, t, n) => a(async () => {
70
+ let n = await import("node:fs/promises"), { dirname: r } = await import("node:path"), { randomUUID: a } = await import("node:crypto"), o = await i(e), s = r(o);
71
+ await n.mkdir(s, { recursive: !0 });
72
+ let c = `${o}.tmp-${a()}`;
73
+ try {
74
+ await n.writeFile(c, t), await n.rename(c, o);
75
+ } catch (e) {
76
+ throw await n.rm(c, { force: !0 }).catch(() => {}), e;
77
+ }
78
+ return { size: t.byteLength };
79
+ }, e, !0),
80
+ get: (e) => a(async () => {
81
+ let t = await (await import("node:fs/promises")).readFile(await i(e));
82
+ return {
83
+ bytes: new Uint8Array(t),
84
+ contentType: "application/octet-stream"
85
+ };
86
+ }, e),
87
+ delete: (e) => a(async () => {
88
+ await (await import("node:fs/promises")).rm(await i(e), { force: !0 });
89
+ }, e),
90
+ publicUrl: () => null,
91
+ getUrl: () => h.succeed(""),
92
+ head: (e) => h.tryPromise({
93
+ try: async () => {
94
+ let t = await import("node:fs/promises");
95
+ try {
96
+ return { size: (await t.stat(await i(e))).size };
97
+ } catch (e) {
98
+ if (e?.code === "ENOENT") return null;
99
+ throw e;
100
+ }
101
+ },
102
+ catch: (e) => g("filesystem", String(e?.message ?? e), !1)
103
+ }),
104
+ getStream: (e) => a(async () => {
105
+ let t = await import("node:fs/promises"), { createReadStream: n } = await import("node:fs"), r = await i(e), a = await t.stat(r);
106
+ return {
107
+ stream: c("filesystem", () => n(r)),
108
+ contentType: "application/octet-stream",
109
+ size: a.size
110
+ };
111
+ }, e),
112
+ putStream: (e, t, n) => a(async () => {
113
+ let n = await import("node:fs/promises"), { dirname: r } = await import("node:path"), { randomUUID: a } = await import("node:crypto"), { createWriteStream: o } = await import("node:fs"), { pipeline: s } = await import("node:stream/promises"), c = await i(e);
114
+ await n.mkdir(r(c), { recursive: !0 });
115
+ let u = `${c}.tmp-${a()}`;
116
+ try {
117
+ await s(await l(t), o(u));
118
+ let e = await n.stat(u);
119
+ return await n.rename(u, c), { size: e.size };
120
+ } catch (e) {
121
+ throw await n.rm(u, { force: !0 }).catch(() => {}), e;
122
+ }
123
+ }, e, !0),
124
+ getRange: (e, t, n) => a(async () => {
125
+ let { createReadStream: r } = await import("node:fs"), a = await i(e), o = [];
126
+ return await new Promise((e, i) => {
127
+ let s = r(a, {
128
+ start: t,
129
+ end: n
130
+ });
131
+ s.on("data", (e) => o.push(e)), s.on("end", () => e()), s.on("error", i);
132
+ }), s(o);
133
+ }, e)
134
+ };
135
+ }, w = (e) => {
136
+ let t = e.name ?? "s3", i = e.publicAcl ?? !0, a = null, o = () => a || (a = (async () => {
137
+ let t, n;
138
+ try {
139
+ t = await import("@aws-sdk/client-s3"), n = await import("@aws-sdk/s3-request-presigner");
140
+ } catch (e) {
141
+ throw Error("the 's3'/'minio' storage provider requires the '@aws-sdk/client-s3' + '@aws-sdk/s3-request-presigner' optional dependencies. Install them to enable it.", { cause: e });
142
+ }
143
+ return {
144
+ client: new t.S3Client({
145
+ ...e.region ? { region: e.region } : { region: "us-east-1" },
146
+ ...e.endpoint ? { endpoint: e.endpoint } : {},
147
+ ...e.forcePathStyle ? { forcePathStyle: !0 } : {},
148
+ ...e.accessKeyId && e.secretAccessKey ? { credentials: {
149
+ accessKeyId: e.accessKeyId,
150
+ secretAccessKey: e.secretAccessKey
151
+ } } : {}
152
+ }),
153
+ cmds: t,
154
+ presign: n.getSignedUrl
155
+ };
156
+ })(), a), s = (e) => h.tryPromise({
157
+ try: e,
158
+ catch: (e) => {
159
+ let n = e?.$metadata?.httpStatusCode;
160
+ return g(t, String(e?.message ?? e), _(n), n);
161
+ }
162
+ }), u = async (e) => {
163
+ let t = e;
164
+ if (typeof t.transformToByteArray == "function") return t.transformToByteArray();
165
+ let n = [];
166
+ for await (let e of t) n.push(e);
167
+ let r = n.reduce((e, t) => e + t.byteLength, 0), i = new Uint8Array(r), a = 0;
168
+ for (let e of n) i.set(e, a), a += e.byteLength;
169
+ return i;
170
+ };
171
+ return {
172
+ name: t,
173
+ objectNamespace: h.succeed(r([
174
+ "s3",
175
+ e.endpoint ? n(e.endpoint) : "aws-s3",
176
+ e.bucket
177
+ ])),
178
+ presigns: !0,
179
+ put: (t, n, r) => s(async () => {
180
+ let { client: a, cmds: s } = await o();
181
+ return await a.send(new s.PutObjectCommand({
182
+ Bucket: e.bucket,
183
+ Key: t,
184
+ Body: n,
185
+ ContentType: r.contentType,
186
+ ...r.public && i ? { ACL: "public-read" } : {}
187
+ })), { size: n.byteLength };
188
+ }),
189
+ get: (t) => s(async () => {
190
+ let { client: n, cmds: r } = await o(), i = await n.send(new r.GetObjectCommand({
191
+ Bucket: e.bucket,
192
+ Key: t
193
+ }));
194
+ return {
195
+ bytes: await u(i.Body),
196
+ contentType: i.ContentType ?? "application/octet-stream"
197
+ };
198
+ }),
199
+ delete: (t) => s(async () => {
200
+ let { client: n, cmds: r } = await o();
201
+ await n.send(new r.DeleteObjectCommand({
202
+ Bucket: e.bucket,
203
+ Key: t
204
+ }));
205
+ }),
206
+ publicUrl: (t) => e.cdnBaseUrl ? `${e.cdnBaseUrl.replace(/\/$/, "")}/${t}` : null,
207
+ ...i ? { setObjectPublic: (t, n) => s(async () => {
208
+ let { client: r, cmds: i } = await o();
209
+ await r.send(new i.PutObjectAclCommand({
210
+ Bucket: e.bucket,
211
+ Key: t,
212
+ ACL: n ? "public-read" : "private"
213
+ }));
214
+ }) } : {},
215
+ getUrl: (t, n) => s(async () => {
216
+ let { client: r, cmds: i, presign: a } = await o();
217
+ return a(r, new i.GetObjectCommand({
218
+ Bucket: e.bucket,
219
+ Key: t
220
+ }), { expiresIn: n });
221
+ }),
222
+ getUploadUrl: (t, n, r) => s(async () => {
223
+ let { client: i, cmds: a, presign: s } = await o();
224
+ return s(i, new a.PutObjectCommand({
225
+ Bucket: e.bucket,
226
+ Key: t,
227
+ ContentType: r
228
+ }), { expiresIn: n });
229
+ }),
230
+ createMultipartUpload: (t, n, r) => s(async () => {
231
+ let { client: a, cmds: s } = await o(), c = await a.send(new s.CreateMultipartUploadCommand({
232
+ Bucket: e.bucket,
233
+ Key: t,
234
+ ContentType: n,
235
+ ...r?.public && i ? { ACL: "public-read" } : {}
236
+ }));
237
+ if (!c.UploadId) throw Error("s3 createMultipartUpload returned no UploadId");
238
+ return { uploadId: c.UploadId };
239
+ }),
240
+ presignUploadPart: (t, n, r, i) => s(async () => {
241
+ let { client: a, cmds: s, presign: c } = await o();
242
+ return c(a, new s.UploadPartCommand({
243
+ Bucket: e.bucket,
244
+ Key: t,
245
+ UploadId: n,
246
+ PartNumber: r
247
+ }), { expiresIn: i });
248
+ }),
249
+ completeMultipartUpload: (t, n, r) => s(async () => {
250
+ let { client: i, cmds: a } = await o(), s = await i.send(new a.CompleteMultipartUploadCommand({
251
+ Bucket: e.bucket,
252
+ Key: t,
253
+ UploadId: n,
254
+ MultipartUpload: { Parts: r.map((e) => ({
255
+ PartNumber: e.partNumber,
256
+ ETag: e.etag
257
+ })) }
258
+ }));
259
+ return s.ETag === void 0 ? {} : { etag: s.ETag };
260
+ }),
261
+ abortMultipartUpload: (t, n) => s(async () => {
262
+ let { client: r, cmds: i } = await o();
263
+ await r.send(new i.AbortMultipartUploadCommand({
264
+ Bucket: e.bucket,
265
+ Key: t,
266
+ UploadId: n
267
+ }));
268
+ }),
269
+ head: (n) => h.tryPromise({
270
+ try: async () => {
271
+ let { client: t, cmds: r } = await o();
272
+ try {
273
+ let i = await t.send(new r.HeadObjectCommand({
274
+ Bucket: e.bucket,
275
+ Key: n
276
+ }));
277
+ return {
278
+ size: i.ContentLength ?? 0,
279
+ ...i.ETag === void 0 ? {} : { etag: i.ETag }
280
+ };
281
+ } catch (e) {
282
+ if (e?.$metadata?.httpStatusCode === 404) return null;
283
+ throw e;
284
+ }
285
+ },
286
+ catch: (e) => {
287
+ let n = e?.$metadata?.httpStatusCode;
288
+ return g(t, String(e?.message ?? e), _(n), n);
289
+ }
290
+ }),
291
+ getStream: (n) => s(async () => {
292
+ let { client: r, cmds: i } = await o(), a = await r.send(new i.GetObjectCommand({
293
+ Bucket: e.bucket,
294
+ Key: n
295
+ })), s = a.Body;
296
+ return {
297
+ stream: c(t, () => s),
298
+ contentType: a.ContentType ?? "application/octet-stream",
299
+ ...a.ContentLength === void 0 ? {} : { size: a.ContentLength }
300
+ };
301
+ }),
302
+ putStream: (t, n, r) => s(async () => {
303
+ let { client: a, cmds: s } = await o(), c;
304
+ try {
305
+ c = await import("@aws-sdk/lib-storage");
306
+ } catch (e) {
307
+ throw Error("s3/minio `putStream` requires the '@aws-sdk/lib-storage' optional dependency (multipart upload). Install it to enable streamed uploads.", { cause: e });
308
+ }
309
+ let u = await l(n);
310
+ return await new c.Upload({
311
+ client: a,
312
+ params: {
313
+ Bucket: e.bucket,
314
+ Key: t,
315
+ Body: u,
316
+ ContentType: r.contentType,
317
+ ...r.public && i ? { ACL: "public-read" } : {}
318
+ }
319
+ }).done(), { size: r.totalSize ?? (await a.send(new s.HeadObjectCommand({
320
+ Bucket: e.bucket,
321
+ Key: t
322
+ }))).ContentLength ?? 0 };
323
+ }),
324
+ getRange: (t, n, r) => s(async () => {
325
+ let { client: i, cmds: a } = await o(), s = await i.send(new a.GetObjectCommand({
326
+ Bucket: e.bucket,
327
+ Key: t,
328
+ Range: `bytes=${n}-${r}`
329
+ }));
330
+ return u(s.Body);
331
+ })
332
+ };
333
+ }, T = (e) => e instanceof Uint8Array ? e : typeof e == "string" ? new Uint8Array(Buffer.from(e, "base64")) : new Uint8Array(e), E = () => {
334
+ let e = null, t = () => {
335
+ if (!e) throw p("the 'database' storage provider isn't bound to a DataStore yet");
336
+ return e;
337
+ }, n = (e) => h.tryPromise({
338
+ try: e,
339
+ catch: (e) => d(e, "database", !0)
340
+ }), a = async (e, n) => (await t().query({
341
+ table: i,
342
+ predicate: {
343
+ column: "key",
344
+ op: "eq",
345
+ value: e
346
+ },
347
+ order: [],
348
+ take: 1,
349
+ skip: void 0,
350
+ projection: n
351
+ }))[0];
352
+ return {
353
+ name: "database",
354
+ objectNamespace: h.succeed(r(["database", i])),
355
+ presigns: !1,
356
+ bindDataStore: (t) => {
357
+ e = t;
358
+ },
359
+ put: (e, r, o) => n(async () => {
360
+ let n = await a(e);
361
+ return n ? await t().update(i, String(n.id), {
362
+ data: r,
363
+ contentType: o.contentType,
364
+ size: String(r.byteLength)
365
+ }) : await t().insert(i, {
366
+ id: m({
367
+ kind: "typeid",
368
+ prefix: "blob"
369
+ }, i),
370
+ key: e,
371
+ data: r,
372
+ contentType: o.contentType,
373
+ size: String(r.byteLength)
374
+ }), { size: r.byteLength };
375
+ }),
376
+ get: (e) => h.suspend(() => n(() => a(e)).pipe(h.flatMap((t) => t ? h.succeed({
377
+ bytes: T(t.data),
378
+ contentType: String(t.contentType)
379
+ }) : h.fail(g("database", `no object at key '${e}'`, !1, 404))))),
380
+ delete: (e) => n(async () => {
381
+ let n = await a(e, ["id"]);
382
+ n && await t().delete(i, String(n.id));
383
+ }),
384
+ publicUrl: () => null,
385
+ getUrl: () => h.succeed(""),
386
+ head: (e) => n(async () => {
387
+ let t = await a(e, ["size"]);
388
+ return t ? { size: o(t.size, "blob size") } : null;
389
+ })
390
+ };
391
+ }, D = (e) => {
392
+ let t = null, i = () => t || (t = (async () => {
393
+ let t;
394
+ try {
395
+ t = await import("@azure/storage-blob");
396
+ } catch (e) {
397
+ throw Error("the 'azure' storage provider requires the '@azure/storage-blob' optional dependency. Install it to enable it.", { cause: e });
398
+ }
399
+ let n = u.read("AZURE_STORAGE_ACCOUNT", e.accountName), r = u.read("AZURE_STORAGE_KEY", e.accountKey), i = n && r ? new t.StorageSharedKeyCredential(n, r) : null, a = u.read("AZURE_STORAGE_CONNECTION_STRING", e.connectionString);
400
+ return {
401
+ container: (a ? t.BlobServiceClient.fromConnectionString(a) : new t.BlobServiceClient(e.endpoint ?? `https://${n}.blob.core.windows.net`, i ?? void 0)).getContainerClient(e.container),
402
+ sdk: t,
403
+ cred: i
404
+ };
405
+ })(), t), a = (e) => h.tryPromise({
406
+ try: e,
407
+ catch: (e) => {
408
+ let t = e?.statusCode;
409
+ return g("azure", String(e?.message ?? e), _(t), t);
410
+ }
411
+ }), o = async (e) => {
412
+ if (!e) return /* @__PURE__ */ new Uint8Array();
413
+ let t = [];
414
+ for await (let n of e) t.push(typeof n == "string" ? new TextEncoder().encode(n) : new Uint8Array(n));
415
+ let n = t.reduce((e, t) => e + t.byteLength, 0), r = new Uint8Array(n), i = 0;
416
+ for (let e of t) r.set(e, i), i += e.byteLength;
417
+ return r;
418
+ }, s = async (t, n, r) => {
419
+ let { container: a, sdk: o, cred: s } = await i();
420
+ if (!s) throw Error("azure SAS requires accountName + accountKey (set them or AZURE_STORAGE_ACCOUNT/AZURE_STORAGE_KEY)");
421
+ let c = a.getBlockBlobClient(t), l = o.generateBlobSASQueryParameters({
422
+ containerName: e.container,
423
+ blobName: t,
424
+ permissions: o.BlobSASPermissions.parse(n),
425
+ startsOn: /* @__PURE__ */ new Date(Date.now() - 6e4),
426
+ expiresOn: new Date(Date.now() + r * 1e3)
427
+ }, s).toString();
428
+ return `${c.url}?${l}`;
429
+ };
430
+ return {
431
+ name: "azure",
432
+ objectNamespace: a(async () => {
433
+ let { container: e } = await i();
434
+ return r(["azure", n(e.url)]);
435
+ }),
436
+ presigns: !0,
437
+ put: (e, t, n) => a(async () => {
438
+ let { container: r } = await i();
439
+ return await r.getBlockBlobClient(e).uploadData(Buffer.from(t), { blobHTTPHeaders: { blobContentType: n.contentType } }), { size: t.byteLength };
440
+ }),
441
+ get: (e) => a(async () => {
442
+ let { container: t } = await i(), n = await t.getBlockBlobClient(e).download();
443
+ return {
444
+ bytes: await o(n.readableStreamBody),
445
+ contentType: n.contentType ?? "application/octet-stream"
446
+ };
447
+ }),
448
+ delete: (e) => a(async () => {
449
+ let { container: t } = await i();
450
+ await t.getBlockBlobClient(e).deleteIfExists();
451
+ }),
452
+ publicUrl: (t) => e.cdnBaseUrl ? `${e.cdnBaseUrl.replace(/\/$/, "")}/${t}` : null,
453
+ getUrl: (e, t) => a(() => s(e, "r", t)),
454
+ getUploadUrl: (e, t, n) => a(() => s(e, "cw", t)),
455
+ head: (e) => h.tryPromise({
456
+ try: async () => {
457
+ let { container: t } = await i();
458
+ try {
459
+ let n = await t.getBlockBlobClient(e).getProperties();
460
+ return {
461
+ size: n.contentLength ?? 0,
462
+ ...n.etag === void 0 ? {} : { etag: n.etag }
463
+ };
464
+ } catch (e) {
465
+ if (e?.statusCode === 404) return null;
466
+ throw e;
467
+ }
468
+ },
469
+ catch: (e) => {
470
+ let t = e?.statusCode;
471
+ return g("azure", String(e?.message ?? e), _(t), t);
472
+ }
473
+ }),
474
+ getStream: (e) => a(async () => {
475
+ let { container: t } = await i(), n = await t.getBlockBlobClient(e).download();
476
+ return {
477
+ stream: c("azure", () => n.readableStreamBody),
478
+ contentType: n.contentType ?? "application/octet-stream",
479
+ ...n.contentLength === void 0 ? {} : { size: n.contentLength }
480
+ };
481
+ }),
482
+ putStream: (e, t, n) => a(async () => {
483
+ let { container: r } = await i(), a = await l(t);
484
+ return await r.getBlockBlobClient(e).uploadStream(a, 4194304, 5, { blobHTTPHeaders: { blobContentType: n.contentType } }), { size: n.totalSize ?? (await r.getBlockBlobClient(e).getProperties()).contentLength ?? 0 };
485
+ }),
486
+ getRange: (e, t, n) => a(async () => {
487
+ let { container: r } = await i(), a = await r.getBlockBlobClient(e).download(t, n - t + 1);
488
+ return o(a.readableStreamBody);
489
+ })
490
+ };
491
+ };
492
+ //#endregion
493
+ export { D as azureProvider, x as clearStorageBuffer, E as databaseProvider, C as filesystemProvider, S as memoryProvider, b as readStorageBuffer, w as s3Provider };
@@ -0,0 +1,31 @@
1
+ import { storageObjectNamespace as e } from "./storageNamespaces.js";
2
+ import { randomUUID as t } from "node:crypto";
3
+ import { authoredError as n, eq as r, table as i, text as a } from "@voltro/database";
4
+ //#region src/referenceCatalogue.ts
5
+ var o = "_voltro_storage_reference_catalogue", s = i(o, {
6
+ id: a().maxLength(64),
7
+ namespace: a().maxLength(64)
8
+ }).primaryKey(["id"]).nonReactive(), c = async (n) => {
9
+ await n.insertIgnoreWithOutcome(o, {
10
+ id: "catalogue",
11
+ namespace: e(["reference-catalogue", t()])
12
+ }, { conflictColumns: ["id"] }), await l(n);
13
+ }, l = async (t, i) => {
14
+ let a = await t.query({
15
+ table: o,
16
+ predicate: r("id", "catalogue"),
17
+ order: [],
18
+ take: 2,
19
+ skip: void 0,
20
+ projection: void 0
21
+ }), s = a[0]?.namespace;
22
+ if (a.length !== 1 || typeof s != "string" || !/^[a-f0-9]{64}$/.test(s)) throw n("Storage reference catalogue identity is missing or invalid; initialize it after normal schema reconciliation before serving inspection");
23
+ return i ? e([
24
+ "reference-view",
25
+ s,
26
+ i.storageInstance,
27
+ i.objectNamespace
28
+ ]) : s;
29
+ };
30
+ //#endregion
31
+ export { o as STORAGE_REFERENCE_CATALOGUE_TABLE, c as ensureStorageReferenceCatalogue, l as storageReferenceCatalogueId, s as storageReferenceCatalogueTable };