@constructive-io/graphql-query 4.14.1 → 4.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.
package/esm/index.js CHANGED
@@ -29,5 +29,7 @@ export * from './generators';
29
29
  export * from './client';
30
30
  // Introspection utilities (infer-tables, transform, transform-schema, schema-query)
31
31
  export * from './introspect';
32
+ // Dynamic storage client (discover planes from _meta, build upload documents)
33
+ export * from './storage';
32
34
  // Utility functions
33
35
  export { parseSmartTags, stripSmartComments } from './utils';
@@ -13,6 +13,7 @@ export function convertFromMetaSchema(metaSchema) {
13
13
  primaryConstraints: pickArrayConstraint(table.primaryKeyConstraints),
14
14
  uniqueConstraints: pickArrayConstraint(table.uniqueConstraints),
15
15
  foreignConstraints: pickForeignConstraint(table.foreignKeyConstraints, table.relations),
16
+ ...(table.storage ? { storage: table.storage } : {}),
16
17
  });
17
18
  }
18
19
  return result;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Runtime dynamic storage client — a thin composition of the three modular
3
+ * pieces: `_meta` discovery, document building, and upload orchestration.
4
+ *
5
+ * The caller never names a mutation, input type, or bucket query field.
6
+ */
7
+ import { findStorageSurface, resolveStorageSurfaces, STORAGE_META_QUERY } from './meta';
8
+ import { uploadToSurface } from './upload';
9
+ export function createStorageClient(options) {
10
+ const { execute, transport } = options;
11
+ let cachedSurfaces = null;
12
+ async function discover() {
13
+ if (cachedSurfaces)
14
+ return cachedSurfaces;
15
+ const result = (await execute(STORAGE_META_QUERY, {}));
16
+ cachedSurfaces = resolveStorageSurfaces(result);
17
+ return cachedSurfaces;
18
+ }
19
+ async function surface(selector) {
20
+ return findStorageSurface(await discover(), selector);
21
+ }
22
+ async function upload(selector, uploadOptions) {
23
+ if (!transport) {
24
+ throw new Error('STORAGE_TRANSPORT_MISSING: createStorageClient({ transport }) is required to upload; ' +
25
+ 'pass the adapter from @constructive-io/upload-client or your own StorageTransport');
26
+ }
27
+ return uploadToSurface(await surface(selector), uploadOptions, { execute, transport });
28
+ }
29
+ return { discover, surface, upload };
30
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Dynamic GraphQL document construction for storage surfaces.
3
+ *
4
+ * Every name in the emitted document comes from the plane's `_meta`-reported
5
+ * surface — nothing here assembles or guesses a GraphQL name.
6
+ */
7
+ import * as t from 'gql-ast';
8
+ import { OperationTypeNode, print } from 'graphql';
9
+ const UPLOAD_PAYLOAD_FIELDS = [
10
+ 'uploadUrl',
11
+ 'fileId',
12
+ 'key',
13
+ 'deduplicated',
14
+ 'expiresAt',
15
+ 'previousVersionId',
16
+ ];
17
+ /**
18
+ * Build the single-file upload mutation document for a storage plane:
19
+ *
20
+ * mutation UploadAppFileMutation($input: UploadAppFileInput!) {
21
+ * uploadAppFile(input: $input) {
22
+ * uploadUrl fileId key deduplicated expiresAt previousVersionId
23
+ * }
24
+ * }
25
+ */
26
+ export function buildUploadDocument(surface) {
27
+ const { mutation, inputType } = surface.upload;
28
+ const ast = t.document({
29
+ definitions: [
30
+ t.operationDefinition({
31
+ operation: OperationTypeNode.MUTATION,
32
+ name: `${capitalize(mutation)}Mutation`,
33
+ variableDefinitions: [
34
+ t.variableDefinition({
35
+ variable: t.variable({ name: 'input' }),
36
+ type: t.nonNullType({ type: t.namedType({ type: inputType }) }),
37
+ }),
38
+ ],
39
+ selectionSet: t.selectionSet({
40
+ selections: [
41
+ t.field({
42
+ name: mutation,
43
+ args: [t.argument({ name: 'input', value: t.variable({ name: 'input' }) })],
44
+ selectionSet: t.selectionSet({
45
+ selections: UPLOAD_PAYLOAD_FIELDS.map((name) => t.field({ name })),
46
+ }),
47
+ }),
48
+ ],
49
+ }),
50
+ }),
51
+ ],
52
+ });
53
+ return print(ast);
54
+ }
55
+ /**
56
+ * Build the download-URL query document for a file row on a storage plane,
57
+ * looked up through the files type's single-row query field reported by
58
+ * `_meta` (`query.one`, e.g. `appFile`), selecting the plane's computed
59
+ * download field.
60
+ */
61
+ export function buildDownloadUrlDocument(surface) {
62
+ if (!surface.downloadUrlField) {
63
+ throw new Error(`STORAGE_SURFACE_NO_DOWNLOAD_FIELD: plane ${surface.filesType} reports no download-URL field`);
64
+ }
65
+ const nodeField = surface.filesNodeField;
66
+ if (!nodeField) {
67
+ throw new Error(`STORAGE_SURFACE_NO_NODE_FIELD: plane ${surface.filesType} reports no single-row query field`);
68
+ }
69
+ const ast = t.document({
70
+ definitions: [
71
+ t.operationDefinition({
72
+ operation: OperationTypeNode.QUERY,
73
+ name: `${surface.filesType}DownloadUrlQuery`,
74
+ variableDefinitions: [
75
+ t.variableDefinition({
76
+ variable: t.variable({ name: 'id' }),
77
+ type: t.nonNullType({ type: t.namedType({ type: 'UUID' }) }),
78
+ }),
79
+ ],
80
+ selectionSet: t.selectionSet({
81
+ selections: [
82
+ t.field({
83
+ name: nodeField,
84
+ args: [t.argument({ name: 'id', value: t.variable({ name: 'id' }) })],
85
+ selectionSet: t.selectionSet({
86
+ selections: [t.field({ name: 'id' }), t.field({ name: surface.downloadUrlField })],
87
+ }),
88
+ }),
89
+ ],
90
+ }),
91
+ }),
92
+ ],
93
+ });
94
+ return print(ast);
95
+ }
96
+ function capitalize(value) {
97
+ return value.charAt(0).toUpperCase() + value.slice(1);
98
+ }
@@ -0,0 +1,5 @@
1
+ export { createStorageClient } from './client';
2
+ export { buildDownloadUrlDocument, buildUploadDocument } from './document';
3
+ export { findStorageSurface, resolveStorageSurfaces, STORAGE_META_QUERY } from './meta';
4
+ export { StorageError } from './transport';
5
+ export { uploadToSurface } from './upload';
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Storage-surface discovery from the `_meta` root query.
3
+ *
4
+ * `_meta` is the semantic storage contract: graphile-meta reports each storage
5
+ * plane's paired tables and upload surface from the registry's own FK facts,
6
+ * so resolving here can never disagree with the emitted schema.
7
+ */
8
+ /** The `_meta` selection a dynamic storage client needs. */
9
+ export const STORAGE_META_QUERY = `
10
+ query StorageMeta {
11
+ _meta {
12
+ tables {
13
+ name
14
+ tableName
15
+ schemaName
16
+ query {
17
+ one
18
+ }
19
+ storage {
20
+ isFilesTable
21
+ isBucketsTable
22
+ filesType
23
+ bucketsType
24
+ downloadUrlField
25
+ upload {
26
+ mutation
27
+ inputType
28
+ payloadType
29
+ bulkMutation
30
+ bulkInputType
31
+ bulkPayloadType
32
+ bulkFileInputType
33
+ bulkFilePayloadType
34
+ requiresOwnerId
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ `.trim();
41
+ /**
42
+ * Resolve every storage plane from a `_meta` response. Both sides of a plane
43
+ * (files and buckets tables) report the same surface; this pairs them back up
44
+ * and fails loudly on any inconsistency rather than returning a partial plane.
45
+ */
46
+ export function resolveStorageSurfaces(result) {
47
+ const tables = result?._meta?.tables;
48
+ if (!Array.isArray(tables)) {
49
+ throw new Error('STORAGE_META_MALFORMED: _meta.tables missing from response');
50
+ }
51
+ const filesByType = new Map();
52
+ const bucketsByType = new Map();
53
+ for (const table of tables) {
54
+ if (!table.storage)
55
+ continue;
56
+ const { isFilesTable, isBucketsTable, filesType } = table.storage;
57
+ if (isFilesTable) {
58
+ const existing = filesByType.get(filesType);
59
+ if (existing) {
60
+ throw new Error(`STORAGE_META_MALFORMED: two files tables (${existing.schemaName}.${existing.tableName}, ` +
61
+ `${table.schemaName}.${table.tableName}) report the same plane ${filesType}`);
62
+ }
63
+ filesByType.set(filesType, table);
64
+ }
65
+ else if (isBucketsTable) {
66
+ const existing = bucketsByType.get(filesType);
67
+ if (existing) {
68
+ throw new Error(`STORAGE_META_MALFORMED: two buckets tables (${existing.schemaName}.${existing.tableName}, ` +
69
+ `${table.schemaName}.${table.tableName}) report the same plane ${filesType}`);
70
+ }
71
+ bucketsByType.set(filesType, table);
72
+ }
73
+ else {
74
+ throw new Error(`STORAGE_META_MALFORMED: table ${table.schemaName}.${table.tableName} carries storage ` +
75
+ `metadata but is neither a files nor a buckets table`);
76
+ }
77
+ }
78
+ for (const [filesType, bucketsTable] of bucketsByType) {
79
+ if (!filesByType.has(filesType)) {
80
+ throw new Error(`STORAGE_META_MALFORMED: buckets table ${bucketsTable.schemaName}.${bucketsTable.tableName} ` +
81
+ `reports plane ${filesType} but no files table does`);
82
+ }
83
+ }
84
+ const surfaces = [];
85
+ for (const [filesType, filesTable] of filesByType) {
86
+ const storage = filesTable.storage;
87
+ const bucketsTable = bucketsByType.get(filesType) ?? null;
88
+ surfaces.push({
89
+ filesType,
90
+ bucketsType: storage.bucketsType,
91
+ filesTable: tableRef(filesTable),
92
+ bucketsTable: bucketsTable ? tableRef(bucketsTable) : null,
93
+ filesNodeField: filesTable.query?.one ?? null,
94
+ downloadUrlField: storage.downloadUrlField,
95
+ upload: storage.upload,
96
+ });
97
+ }
98
+ return surfaces;
99
+ }
100
+ /**
101
+ * Find exactly one storage plane by semantic coordinates. Throws when the
102
+ * selector matches nothing or more than one plane.
103
+ */
104
+ export function findStorageSurface(surfaces, selector) {
105
+ if (!selector.filesTable && !selector.filesType && !selector.schemaName) {
106
+ throw new Error('STORAGE_SURFACE_SELECTOR_EMPTY: provide filesTable, filesType, and/or schemaName');
107
+ }
108
+ const matches = surfaces.filter((surface) => (selector.filesTable === undefined || surface.filesTable.tableName === selector.filesTable) &&
109
+ (selector.schemaName === undefined || surface.filesTable.schemaName === selector.schemaName) &&
110
+ (selector.filesType === undefined || surface.filesType === selector.filesType));
111
+ if (matches.length === 0) {
112
+ throw new Error(`STORAGE_SURFACE_NOT_FOUND: no storage plane matches ${JSON.stringify(selector)}; ` +
113
+ `known planes: ${surfaces.map((s) => `${s.filesTable.schemaName}.${s.filesTable.tableName}`).join(', ') || '(none)'}`);
114
+ }
115
+ if (matches.length > 1) {
116
+ throw new Error(`STORAGE_SURFACE_AMBIGUOUS: ${matches.length} storage planes match ${JSON.stringify(selector)}: ` +
117
+ matches.map((s) => `${s.filesTable.schemaName}.${s.filesTable.tableName}`).join(', '));
118
+ }
119
+ return matches[0];
120
+ }
121
+ function tableRef(table) {
122
+ return { name: table.name, tableName: table.tableName, schemaName: table.schemaName };
123
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Ports the storage orchestration depends on.
3
+ *
4
+ * Byte-level concerns — hashing a file, PUTting it to a presigned URL — are
5
+ * injected as an adapter rather than imported, so this package stays a
6
+ * GraphQL discovery/document library with no upload or S3 dependency.
7
+ * `@constructive-io/upload-client` ships an adapter satisfying
8
+ * `StorageTransport`; any other implementation works equally well.
9
+ */
10
+ export class StorageError extends Error {
11
+ code;
12
+ cause;
13
+ constructor(code, message, cause) {
14
+ super(message);
15
+ this.name = 'StorageError';
16
+ this.code = code;
17
+ this.cause = cause;
18
+ }
19
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Dynamic storage-surface types.
3
+ *
4
+ * These mirror the `_meta` storage payload emitted by graphile-meta, which in
5
+ * turn derives from the same registry facts (files→buckets FK pairing,
6
+ * inflection) the presigned-url plugin emits the schema from. A client that
7
+ * consumes these never guesses a GraphQL name.
8
+ */
9
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Upload orchestration for one already-resolved storage plane:
3
+ * hash → dynamic upload mutation → presigned PUT.
4
+ *
5
+ * Standalone on purpose — callers that resolved a surface themselves can use
6
+ * this without the client wrapper, and the byte-level work is an injected
7
+ * `StorageTransport`.
8
+ */
9
+ import { buildUploadDocument } from './document';
10
+ import { StorageError } from './transport';
11
+ export async function uploadToSurface(surface, options, context) {
12
+ const { execute, transport } = context;
13
+ const { file, bucketKey, key, isPublic, ownerId, signal } = options;
14
+ if (!file) {
15
+ throw new StorageError('INVALID_FILE', 'No file provided');
16
+ }
17
+ if (file.size <= 0) {
18
+ throw new StorageError('INVALID_FILE', 'File is empty');
19
+ }
20
+ if (surface.upload.requiresOwnerId && !ownerId) {
21
+ throw new StorageError('OWNER_REQUIRED', `Storage plane ${surface.filesType} is entity-keyed and requires ownerId`);
22
+ }
23
+ checkAborted(signal);
24
+ const contentHash = await transport.hashFile(file);
25
+ checkAborted(signal);
26
+ const contentType = file.type || 'application/octet-stream';
27
+ const input = {
28
+ contentHash,
29
+ contentType,
30
+ size: file.size,
31
+ filename: file.name || undefined,
32
+ };
33
+ if (bucketKey !== undefined)
34
+ input.bucketKey = bucketKey;
35
+ if (key !== undefined)
36
+ input.key = key;
37
+ if (isPublic !== undefined)
38
+ input.isPublic = isPublic;
39
+ if (ownerId !== undefined)
40
+ input.ownerId = ownerId;
41
+ let data;
42
+ try {
43
+ data = await execute(buildUploadDocument(surface), { input });
44
+ }
45
+ catch (err) {
46
+ throw new StorageError('UPLOAD_MUTATION_FAILED', `${surface.upload.mutation} mutation failed: ${err instanceof Error ? err.message : String(err)}`, err);
47
+ }
48
+ const payload = data?.[surface.upload.mutation];
49
+ if (!payload) {
50
+ throw new StorageError('UPLOAD_MUTATION_FAILED', `No data returned from ${surface.upload.mutation}`);
51
+ }
52
+ if (payload.deduplicated) {
53
+ return toResult(payload);
54
+ }
55
+ if (!payload.uploadUrl) {
56
+ throw new StorageError('UPLOAD_MUTATION_FAILED', 'Server returned deduplicated=false but no uploadUrl');
57
+ }
58
+ checkAborted(signal);
59
+ await transport.putObject(payload.uploadUrl, await file.arrayBuffer(), contentType, signal);
60
+ return toResult(payload);
61
+ }
62
+ function toResult(payload) {
63
+ return {
64
+ fileId: payload.fileId,
65
+ key: payload.key,
66
+ deduplicated: payload.deduplicated,
67
+ expiresAt: payload.expiresAt ?? null,
68
+ previousVersionId: payload.previousVersionId ?? null,
69
+ };
70
+ }
71
+ function checkAborted(signal) {
72
+ if (signal?.aborted) {
73
+ throw new StorageError('ABORTED', 'Upload was cancelled');
74
+ }
75
+ }
package/index.d.ts CHANGED
@@ -20,4 +20,5 @@ export { validateMetaObject, type ValidationResult } from './meta-object/validat
20
20
  export * from './generators';
21
21
  export * from './client';
22
22
  export * from './introspect';
23
+ export * from './storage';
23
24
  export { parseSmartTags, stripSmartComments } from './utils';
package/index.js CHANGED
@@ -73,6 +73,8 @@ __exportStar(require("./generators"), exports);
73
73
  __exportStar(require("./client"), exports);
74
74
  // Introspection utilities (infer-tables, transform, transform-schema, schema-query)
75
75
  __exportStar(require("./introspect"), exports);
76
+ // Dynamic storage client (discover planes from _meta, build upload documents)
77
+ __exportStar(require("./storage"), exports);
76
78
  // Utility functions
77
79
  var utils_1 = require("./utils");
78
80
  Object.defineProperty(exports, "parseSmartTags", { enumerable: true, get: function () { return utils_1.parseSmartTags; } });
@@ -1,3 +1,4 @@
1
+ import type { StorageUploadSurface } from '../storage/types';
1
2
  import type { MetaFieldType } from '../types';
2
3
  interface MetaSchemaField {
3
4
  name: string;
@@ -20,6 +21,14 @@ interface MetaSchemaBelongsTo {
20
21
  interface MetaSchemaRelations {
21
22
  belongsTo: MetaSchemaBelongsTo[];
22
23
  }
24
+ interface MetaSchemaStorage {
25
+ isFilesTable: boolean;
26
+ isBucketsTable: boolean;
27
+ filesType: string;
28
+ bucketsType: string;
29
+ downloadUrlField: string | null;
30
+ upload: StorageUploadSurface;
31
+ }
23
32
  interface MetaSchemaTable {
24
33
  name: string;
25
34
  fields: MetaSchemaField[];
@@ -27,6 +36,7 @@ interface MetaSchemaTable {
27
36
  uniqueConstraints: MetaSchemaConstraint[];
28
37
  foreignKeyConstraints: MetaSchemaForeignConstraint[];
29
38
  relations: MetaSchemaRelations;
39
+ storage?: MetaSchemaStorage | null;
30
40
  }
31
41
  interface MetaSchemaInput {
32
42
  _meta: {
@@ -54,6 +64,7 @@ interface ConvertedTable {
54
64
  primaryConstraints: ConvertedConstraint[];
55
65
  uniqueConstraints: ConvertedConstraint[];
56
66
  foreignConstraints: ConvertedForeignConstraint[];
67
+ storage?: MetaSchemaStorage;
57
68
  }
58
69
  interface ConvertedMetaObject {
59
70
  tables: ConvertedTable[];
@@ -16,6 +16,7 @@ function convertFromMetaSchema(metaSchema) {
16
16
  primaryConstraints: pickArrayConstraint(table.primaryKeyConstraints),
17
17
  uniqueConstraints: pickArrayConstraint(table.uniqueConstraints),
18
18
  foreignConstraints: pickForeignConstraint(table.foreignKeyConstraints, table.relations),
19
+ ...(table.storage ? { storage: table.storage } : {}),
19
20
  });
20
21
  }
21
22
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-query",
3
- "version": "4.14.1",
3
+ "version": "4.15.0",
4
4
  "description": "Constructive GraphQL Query",
5
5
  "author": "Constructive <developers@constructive.io>",
6
6
  "main": "index.js",
@@ -38,7 +38,7 @@
38
38
  "grafast": "1.1.2",
39
39
  "graphile-build-pg": "5.1.3",
40
40
  "graphile-config": "1.1.0",
41
- "graphile-settings": "^6.18.1",
41
+ "graphile-settings": "^6.18.2",
42
42
  "graphql": "16.13.0",
43
43
  "inflection": "^3.0.0",
44
44
  "inflekt": "^0.8.1",
@@ -55,5 +55,5 @@
55
55
  "devDependencies": {
56
56
  "makage": "^0.3.0"
57
57
  },
58
- "gitHead": "8d77dfe57bb328cbdfe10732b10ca24865adf9c0"
58
+ "gitHead": "d10fe91ede80a4c2dfe2c1c6dcf6fe7193784eba"
59
59
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Runtime dynamic storage client — a thin composition of the three modular
3
+ * pieces: `_meta` discovery, document building, and upload orchestration.
4
+ *
5
+ * The caller never names a mutation, input type, or bucket query field.
6
+ */
7
+ import type { GraphQLExecutor, StorageTransport } from './transport';
8
+ import type { StorageSurface, StorageSurfaceSelector } from './types';
9
+ import type { StorageUploadOptions, StorageUploadResult } from './upload';
10
+ export interface StorageClientOptions {
11
+ /** GraphQL executor — the only integration point with your GraphQL client */
12
+ execute: GraphQLExecutor;
13
+ /**
14
+ * Byte-level adapter (hash + presigned PUT). Required for `upload()`;
15
+ * omit it when you only need discovery or document building.
16
+ */
17
+ transport?: StorageTransport;
18
+ }
19
+ export interface StorageClient {
20
+ /** Discover every storage plane from `_meta` (cached after the first call) */
21
+ discover(): Promise<StorageSurface[]>;
22
+ /** Resolve exactly one storage plane by semantic coordinates */
23
+ surface(selector: StorageSurfaceSelector): Promise<StorageSurface>;
24
+ /** Upload a file to a plane: hash → dynamic upload mutation → presigned PUT */
25
+ upload(selector: StorageSurfaceSelector, options: StorageUploadOptions): Promise<StorageUploadResult>;
26
+ }
27
+ export declare function createStorageClient(options: StorageClientOptions): StorageClient;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Runtime dynamic storage client — a thin composition of the three modular
4
+ * pieces: `_meta` discovery, document building, and upload orchestration.
5
+ *
6
+ * The caller never names a mutation, input type, or bucket query field.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createStorageClient = createStorageClient;
10
+ const meta_1 = require("./meta");
11
+ const upload_1 = require("./upload");
12
+ function createStorageClient(options) {
13
+ const { execute, transport } = options;
14
+ let cachedSurfaces = null;
15
+ async function discover() {
16
+ if (cachedSurfaces)
17
+ return cachedSurfaces;
18
+ const result = (await execute(meta_1.STORAGE_META_QUERY, {}));
19
+ cachedSurfaces = (0, meta_1.resolveStorageSurfaces)(result);
20
+ return cachedSurfaces;
21
+ }
22
+ async function surface(selector) {
23
+ return (0, meta_1.findStorageSurface)(await discover(), selector);
24
+ }
25
+ async function upload(selector, uploadOptions) {
26
+ if (!transport) {
27
+ throw new Error('STORAGE_TRANSPORT_MISSING: createStorageClient({ transport }) is required to upload; ' +
28
+ 'pass the adapter from @constructive-io/upload-client or your own StorageTransport');
29
+ }
30
+ return (0, upload_1.uploadToSurface)(await surface(selector), uploadOptions, { execute, transport });
31
+ }
32
+ return { discover, surface, upload };
33
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Dynamic GraphQL document construction for storage surfaces.
3
+ *
4
+ * Every name in the emitted document comes from the plane's `_meta`-reported
5
+ * surface — nothing here assembles or guesses a GraphQL name.
6
+ */
7
+ import type { StorageSurface } from './types';
8
+ /**
9
+ * Build the single-file upload mutation document for a storage plane:
10
+ *
11
+ * mutation UploadAppFileMutation($input: UploadAppFileInput!) {
12
+ * uploadAppFile(input: $input) {
13
+ * uploadUrl fileId key deduplicated expiresAt previousVersionId
14
+ * }
15
+ * }
16
+ */
17
+ export declare function buildUploadDocument(surface: StorageSurface): string;
18
+ /**
19
+ * Build the download-URL query document for a file row on a storage plane,
20
+ * looked up through the files type's single-row query field reported by
21
+ * `_meta` (`query.one`, e.g. `appFile`), selecting the plane's computed
22
+ * download field.
23
+ */
24
+ export declare function buildDownloadUrlDocument(surface: StorageSurface): string;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * Dynamic GraphQL document construction for storage surfaces.
4
+ *
5
+ * Every name in the emitted document comes from the plane's `_meta`-reported
6
+ * surface — nothing here assembles or guesses a GraphQL name.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.buildUploadDocument = buildUploadDocument;
43
+ exports.buildDownloadUrlDocument = buildDownloadUrlDocument;
44
+ const t = __importStar(require("gql-ast"));
45
+ const graphql_1 = require("graphql");
46
+ const UPLOAD_PAYLOAD_FIELDS = [
47
+ 'uploadUrl',
48
+ 'fileId',
49
+ 'key',
50
+ 'deduplicated',
51
+ 'expiresAt',
52
+ 'previousVersionId',
53
+ ];
54
+ /**
55
+ * Build the single-file upload mutation document for a storage plane:
56
+ *
57
+ * mutation UploadAppFileMutation($input: UploadAppFileInput!) {
58
+ * uploadAppFile(input: $input) {
59
+ * uploadUrl fileId key deduplicated expiresAt previousVersionId
60
+ * }
61
+ * }
62
+ */
63
+ function buildUploadDocument(surface) {
64
+ const { mutation, inputType } = surface.upload;
65
+ const ast = t.document({
66
+ definitions: [
67
+ t.operationDefinition({
68
+ operation: graphql_1.OperationTypeNode.MUTATION,
69
+ name: `${capitalize(mutation)}Mutation`,
70
+ variableDefinitions: [
71
+ t.variableDefinition({
72
+ variable: t.variable({ name: 'input' }),
73
+ type: t.nonNullType({ type: t.namedType({ type: inputType }) }),
74
+ }),
75
+ ],
76
+ selectionSet: t.selectionSet({
77
+ selections: [
78
+ t.field({
79
+ name: mutation,
80
+ args: [t.argument({ name: 'input', value: t.variable({ name: 'input' }) })],
81
+ selectionSet: t.selectionSet({
82
+ selections: UPLOAD_PAYLOAD_FIELDS.map((name) => t.field({ name })),
83
+ }),
84
+ }),
85
+ ],
86
+ }),
87
+ }),
88
+ ],
89
+ });
90
+ return (0, graphql_1.print)(ast);
91
+ }
92
+ /**
93
+ * Build the download-URL query document for a file row on a storage plane,
94
+ * looked up through the files type's single-row query field reported by
95
+ * `_meta` (`query.one`, e.g. `appFile`), selecting the plane's computed
96
+ * download field.
97
+ */
98
+ function buildDownloadUrlDocument(surface) {
99
+ if (!surface.downloadUrlField) {
100
+ throw new Error(`STORAGE_SURFACE_NO_DOWNLOAD_FIELD: plane ${surface.filesType} reports no download-URL field`);
101
+ }
102
+ const nodeField = surface.filesNodeField;
103
+ if (!nodeField) {
104
+ throw new Error(`STORAGE_SURFACE_NO_NODE_FIELD: plane ${surface.filesType} reports no single-row query field`);
105
+ }
106
+ const ast = t.document({
107
+ definitions: [
108
+ t.operationDefinition({
109
+ operation: graphql_1.OperationTypeNode.QUERY,
110
+ name: `${surface.filesType}DownloadUrlQuery`,
111
+ variableDefinitions: [
112
+ t.variableDefinition({
113
+ variable: t.variable({ name: 'id' }),
114
+ type: t.nonNullType({ type: t.namedType({ type: 'UUID' }) }),
115
+ }),
116
+ ],
117
+ selectionSet: t.selectionSet({
118
+ selections: [
119
+ t.field({
120
+ name: nodeField,
121
+ args: [t.argument({ name: 'id', value: t.variable({ name: 'id' }) })],
122
+ selectionSet: t.selectionSet({
123
+ selections: [t.field({ name: 'id' }), t.field({ name: surface.downloadUrlField })],
124
+ }),
125
+ }),
126
+ ],
127
+ }),
128
+ }),
129
+ ],
130
+ });
131
+ return (0, graphql_1.print)(ast);
132
+ }
133
+ function capitalize(value) {
134
+ return value.charAt(0).toUpperCase() + value.slice(1);
135
+ }
@@ -0,0 +1,10 @@
1
+ export type { StorageClient, StorageClientOptions } from './client';
2
+ export { createStorageClient } from './client';
3
+ export { buildDownloadUrlDocument, buildUploadDocument } from './document';
4
+ export type { StorageMetaResult } from './meta';
5
+ export { findStorageSurface, resolveStorageSurfaces, STORAGE_META_QUERY } from './meta';
6
+ export type { GraphQLExecutor, StorageErrorCode, StorageFile, StorageTransport, } from './transport';
7
+ export { StorageError } from './transport';
8
+ export type { StorageSurface, StorageSurfaceSelector, StorageTableRef, StorageUploadSurface, } from './types';
9
+ export type { StorageUploadOptions, StorageUploadResult, UploadToSurfaceContext, } from './upload';
10
+ export { uploadToSurface } from './upload';
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadToSurface = exports.StorageError = exports.STORAGE_META_QUERY = exports.resolveStorageSurfaces = exports.findStorageSurface = exports.buildUploadDocument = exports.buildDownloadUrlDocument = exports.createStorageClient = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "createStorageClient", { enumerable: true, get: function () { return client_1.createStorageClient; } });
6
+ var document_1 = require("./document");
7
+ Object.defineProperty(exports, "buildDownloadUrlDocument", { enumerable: true, get: function () { return document_1.buildDownloadUrlDocument; } });
8
+ Object.defineProperty(exports, "buildUploadDocument", { enumerable: true, get: function () { return document_1.buildUploadDocument; } });
9
+ var meta_1 = require("./meta");
10
+ Object.defineProperty(exports, "findStorageSurface", { enumerable: true, get: function () { return meta_1.findStorageSurface; } });
11
+ Object.defineProperty(exports, "resolveStorageSurfaces", { enumerable: true, get: function () { return meta_1.resolveStorageSurfaces; } });
12
+ Object.defineProperty(exports, "STORAGE_META_QUERY", { enumerable: true, get: function () { return meta_1.STORAGE_META_QUERY; } });
13
+ var transport_1 = require("./transport");
14
+ Object.defineProperty(exports, "StorageError", { enumerable: true, get: function () { return transport_1.StorageError; } });
15
+ var upload_1 = require("./upload");
16
+ Object.defineProperty(exports, "uploadToSurface", { enumerable: true, get: function () { return upload_1.uploadToSurface; } });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Storage-surface discovery from the `_meta` root query.
3
+ *
4
+ * `_meta` is the semantic storage contract: graphile-meta reports each storage
5
+ * plane's paired tables and upload surface from the registry's own FK facts,
6
+ * so resolving here can never disagree with the emitted schema.
7
+ */
8
+ import type { StorageSurface, StorageSurfaceSelector, StorageUploadSurface } from './types';
9
+ /** The `_meta` selection a dynamic storage client needs. */
10
+ export declare const STORAGE_META_QUERY: string;
11
+ interface StorageMetaTable {
12
+ name: string;
13
+ tableName: string;
14
+ schemaName: string;
15
+ query?: {
16
+ one: string | null;
17
+ } | null;
18
+ storage: {
19
+ isFilesTable: boolean;
20
+ isBucketsTable: boolean;
21
+ filesType: string;
22
+ bucketsType: string;
23
+ downloadUrlField: string | null;
24
+ upload: StorageUploadSurface;
25
+ } | null;
26
+ }
27
+ export interface StorageMetaResult {
28
+ _meta: {
29
+ tables: StorageMetaTable[];
30
+ };
31
+ }
32
+ /**
33
+ * Resolve every storage plane from a `_meta` response. Both sides of a plane
34
+ * (files and buckets tables) report the same surface; this pairs them back up
35
+ * and fails loudly on any inconsistency rather than returning a partial plane.
36
+ */
37
+ export declare function resolveStorageSurfaces(result: StorageMetaResult): StorageSurface[];
38
+ /**
39
+ * Find exactly one storage plane by semantic coordinates. Throws when the
40
+ * selector matches nothing or more than one plane.
41
+ */
42
+ export declare function findStorageSurface(surfaces: StorageSurface[], selector: StorageSurfaceSelector): StorageSurface;
43
+ export {};
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Storage-surface discovery from the `_meta` root query.
4
+ *
5
+ * `_meta` is the semantic storage contract: graphile-meta reports each storage
6
+ * plane's paired tables and upload surface from the registry's own FK facts,
7
+ * so resolving here can never disagree with the emitted schema.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.STORAGE_META_QUERY = void 0;
11
+ exports.resolveStorageSurfaces = resolveStorageSurfaces;
12
+ exports.findStorageSurface = findStorageSurface;
13
+ /** The `_meta` selection a dynamic storage client needs. */
14
+ exports.STORAGE_META_QUERY = `
15
+ query StorageMeta {
16
+ _meta {
17
+ tables {
18
+ name
19
+ tableName
20
+ schemaName
21
+ query {
22
+ one
23
+ }
24
+ storage {
25
+ isFilesTable
26
+ isBucketsTable
27
+ filesType
28
+ bucketsType
29
+ downloadUrlField
30
+ upload {
31
+ mutation
32
+ inputType
33
+ payloadType
34
+ bulkMutation
35
+ bulkInputType
36
+ bulkPayloadType
37
+ bulkFileInputType
38
+ bulkFilePayloadType
39
+ requiresOwnerId
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ `.trim();
46
+ /**
47
+ * Resolve every storage plane from a `_meta` response. Both sides of a plane
48
+ * (files and buckets tables) report the same surface; this pairs them back up
49
+ * and fails loudly on any inconsistency rather than returning a partial plane.
50
+ */
51
+ function resolveStorageSurfaces(result) {
52
+ const tables = result?._meta?.tables;
53
+ if (!Array.isArray(tables)) {
54
+ throw new Error('STORAGE_META_MALFORMED: _meta.tables missing from response');
55
+ }
56
+ const filesByType = new Map();
57
+ const bucketsByType = new Map();
58
+ for (const table of tables) {
59
+ if (!table.storage)
60
+ continue;
61
+ const { isFilesTable, isBucketsTable, filesType } = table.storage;
62
+ if (isFilesTable) {
63
+ const existing = filesByType.get(filesType);
64
+ if (existing) {
65
+ throw new Error(`STORAGE_META_MALFORMED: two files tables (${existing.schemaName}.${existing.tableName}, ` +
66
+ `${table.schemaName}.${table.tableName}) report the same plane ${filesType}`);
67
+ }
68
+ filesByType.set(filesType, table);
69
+ }
70
+ else if (isBucketsTable) {
71
+ const existing = bucketsByType.get(filesType);
72
+ if (existing) {
73
+ throw new Error(`STORAGE_META_MALFORMED: two buckets tables (${existing.schemaName}.${existing.tableName}, ` +
74
+ `${table.schemaName}.${table.tableName}) report the same plane ${filesType}`);
75
+ }
76
+ bucketsByType.set(filesType, table);
77
+ }
78
+ else {
79
+ throw new Error(`STORAGE_META_MALFORMED: table ${table.schemaName}.${table.tableName} carries storage ` +
80
+ `metadata but is neither a files nor a buckets table`);
81
+ }
82
+ }
83
+ for (const [filesType, bucketsTable] of bucketsByType) {
84
+ if (!filesByType.has(filesType)) {
85
+ throw new Error(`STORAGE_META_MALFORMED: buckets table ${bucketsTable.schemaName}.${bucketsTable.tableName} ` +
86
+ `reports plane ${filesType} but no files table does`);
87
+ }
88
+ }
89
+ const surfaces = [];
90
+ for (const [filesType, filesTable] of filesByType) {
91
+ const storage = filesTable.storage;
92
+ const bucketsTable = bucketsByType.get(filesType) ?? null;
93
+ surfaces.push({
94
+ filesType,
95
+ bucketsType: storage.bucketsType,
96
+ filesTable: tableRef(filesTable),
97
+ bucketsTable: bucketsTable ? tableRef(bucketsTable) : null,
98
+ filesNodeField: filesTable.query?.one ?? null,
99
+ downloadUrlField: storage.downloadUrlField,
100
+ upload: storage.upload,
101
+ });
102
+ }
103
+ return surfaces;
104
+ }
105
+ /**
106
+ * Find exactly one storage plane by semantic coordinates. Throws when the
107
+ * selector matches nothing or more than one plane.
108
+ */
109
+ function findStorageSurface(surfaces, selector) {
110
+ if (!selector.filesTable && !selector.filesType && !selector.schemaName) {
111
+ throw new Error('STORAGE_SURFACE_SELECTOR_EMPTY: provide filesTable, filesType, and/or schemaName');
112
+ }
113
+ const matches = surfaces.filter((surface) => (selector.filesTable === undefined || surface.filesTable.tableName === selector.filesTable) &&
114
+ (selector.schemaName === undefined || surface.filesTable.schemaName === selector.schemaName) &&
115
+ (selector.filesType === undefined || surface.filesType === selector.filesType));
116
+ if (matches.length === 0) {
117
+ throw new Error(`STORAGE_SURFACE_NOT_FOUND: no storage plane matches ${JSON.stringify(selector)}; ` +
118
+ `known planes: ${surfaces.map((s) => `${s.filesTable.schemaName}.${s.filesTable.tableName}`).join(', ') || '(none)'}`);
119
+ }
120
+ if (matches.length > 1) {
121
+ throw new Error(`STORAGE_SURFACE_AMBIGUOUS: ${matches.length} storage planes match ${JSON.stringify(selector)}: ` +
122
+ matches.map((s) => `${s.filesTable.schemaName}.${s.filesTable.tableName}`).join(', '));
123
+ }
124
+ return matches[0];
125
+ }
126
+ function tableRef(table) {
127
+ return { name: table.name, tableName: table.tableName, schemaName: table.schemaName };
128
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Ports the storage orchestration depends on.
3
+ *
4
+ * Byte-level concerns — hashing a file, PUTting it to a presigned URL — are
5
+ * injected as an adapter rather than imported, so this package stays a
6
+ * GraphQL discovery/document library with no upload or S3 dependency.
7
+ * `@constructive-io/upload-client` ships an adapter satisfying
8
+ * `StorageTransport`; any other implementation works equally well.
9
+ */
10
+ /**
11
+ * Minimal file interface for hashing and uploading.
12
+ * Compatible with browser `File`, Node.js `Blob`, and custom implementations.
13
+ */
14
+ export interface StorageFile {
15
+ readonly name: string;
16
+ readonly size: number;
17
+ readonly type: string;
18
+ arrayBuffer(): Promise<ArrayBuffer>;
19
+ }
20
+ /** Byte-level adapter: content hashing and presigned PUT. */
21
+ export interface StorageTransport {
22
+ /** SHA-256 the file contents, hex-encoded */
23
+ hashFile(file: StorageFile): Promise<string>;
24
+ /** PUT the bytes to a presigned URL */
25
+ putObject(url: string, body: ArrayBuffer, contentType: string, signal?: AbortSignal): Promise<void>;
26
+ }
27
+ /**
28
+ * Executes a GraphQL operation and returns its `data`.
29
+ * The only integration point with a GraphQL client.
30
+ */
31
+ export type GraphQLExecutor = (query: string, variables: Record<string, unknown>) => Promise<Record<string, unknown>>;
32
+ export type StorageErrorCode = 'INVALID_FILE' | 'OWNER_REQUIRED' | 'HASH_FAILED' | 'UPLOAD_MUTATION_FAILED' | 'PUT_UPLOAD_FAILED' | 'ABORTED';
33
+ export declare class StorageError extends Error {
34
+ readonly code: StorageErrorCode;
35
+ readonly cause?: unknown;
36
+ constructor(code: StorageErrorCode, message: string, cause?: unknown);
37
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * Ports the storage orchestration depends on.
4
+ *
5
+ * Byte-level concerns — hashing a file, PUTting it to a presigned URL — are
6
+ * injected as an adapter rather than imported, so this package stays a
7
+ * GraphQL discovery/document library with no upload or S3 dependency.
8
+ * `@constructive-io/upload-client` ships an adapter satisfying
9
+ * `StorageTransport`; any other implementation works equally well.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.StorageError = void 0;
13
+ class StorageError extends Error {
14
+ code;
15
+ cause;
16
+ constructor(code, message, cause) {
17
+ super(message);
18
+ this.name = 'StorageError';
19
+ this.code = code;
20
+ this.cause = cause;
21
+ }
22
+ }
23
+ exports.StorageError = StorageError;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Dynamic storage-surface types.
3
+ *
4
+ * These mirror the `_meta` storage payload emitted by graphile-meta, which in
5
+ * turn derives from the same registry facts (files→buckets FK pairing,
6
+ * inflection) the presigned-url plugin emits the schema from. A client that
7
+ * consumes these never guesses a GraphQL name.
8
+ */
9
+ /** The GraphQL upload surface of a storage plane, as reported by `_meta`. */
10
+ export interface StorageUploadSurface {
11
+ /** Root mutation field for single-file upload (e.g. `uploadAppFile`) */
12
+ mutation: string;
13
+ /** Input type of the single upload mutation (e.g. `UploadAppFileInput`) */
14
+ inputType: string;
15
+ /** Payload type of the single upload mutation */
16
+ payloadType: string;
17
+ /** Root mutation field for bulk upload */
18
+ bulkMutation: string;
19
+ /** Input type of the bulk upload mutation */
20
+ bulkInputType: string;
21
+ /** Payload type of the bulk upload mutation */
22
+ bulkPayloadType: string;
23
+ /** Per-file input type inside the bulk input */
24
+ bulkFileInputType: string;
25
+ /** Per-file payload type inside the bulk payload */
26
+ bulkFilePayloadType: string;
27
+ /** Whether the upload input requires `ownerId` (entity-keyed plane) */
28
+ requiresOwnerId: boolean;
29
+ }
30
+ /** The pg identity of a `_meta` table entry backing a storage plane side. */
31
+ export interface StorageTableRef {
32
+ /** Final GraphQL type name */
33
+ name: string;
34
+ /** PostgreSQL table name */
35
+ tableName: string;
36
+ /** PostgreSQL schema name */
37
+ schemaName: string;
38
+ }
39
+ /** One storage plane: a paired files/buckets table and its GraphQL surface. */
40
+ export interface StorageSurface {
41
+ /** GraphQL type name of the plane's files table */
42
+ filesType: string;
43
+ /** GraphQL type name of the plane's buckets table */
44
+ bucketsType: string;
45
+ /** The files table's `_meta` identity */
46
+ filesTable: StorageTableRef;
47
+ /** The buckets table's `_meta` identity (null when the buckets table is not exposed) */
48
+ bucketsTable: StorageTableRef | null;
49
+ /** Root query field for a single files row by primary key (from `_meta` query.one) */
50
+ filesNodeField: string | null;
51
+ /** Computed download-URL field on the files type */
52
+ downloadUrlField: string | null;
53
+ /** The plane's GraphQL upload surface */
54
+ upload: StorageUploadSurface;
55
+ }
56
+ /**
57
+ * Selects one storage plane by semantic coordinates. All provided fields must
58
+ * match exactly; the lookup throws when nothing (or more than one plane)
59
+ * matches.
60
+ */
61
+ export interface StorageSurfaceSelector {
62
+ /** PostgreSQL table name of the files table (e.g. `files`, `app_files`) */
63
+ filesTable?: string;
64
+ /** PostgreSQL schema name of the files table */
65
+ schemaName?: string;
66
+ /** GraphQL type name of the files table (e.g. `AppFile`) */
67
+ filesType?: string;
68
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Dynamic storage-surface types.
4
+ *
5
+ * These mirror the `_meta` storage payload emitted by graphile-meta, which in
6
+ * turn derives from the same registry facts (files→buckets FK pairing,
7
+ * inflection) the presigned-url plugin emits the schema from. A client that
8
+ * consumes these never guesses a GraphQL name.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Upload orchestration for one already-resolved storage plane:
3
+ * hash → dynamic upload mutation → presigned PUT.
4
+ *
5
+ * Standalone on purpose — callers that resolved a surface themselves can use
6
+ * this without the client wrapper, and the byte-level work is an injected
7
+ * `StorageTransport`.
8
+ */
9
+ import type { GraphQLExecutor, StorageFile, StorageTransport } from './transport';
10
+ import type { StorageSurface } from './types';
11
+ export interface StorageUploadOptions {
12
+ /** The file to upload (browser File object or compatible) */
13
+ file: StorageFile;
14
+ /** Bucket key within the plane (omit to use the plane's default bucket) */
15
+ bucketKey?: string;
16
+ /** Custom object key (enables versioning of the same key) */
17
+ key?: string;
18
+ /** Whether the file should be publicly readable */
19
+ isPublic?: boolean;
20
+ /** Owner identity — required when the plane is entity-keyed */
21
+ ownerId?: string;
22
+ /** AbortSignal for cancellation */
23
+ signal?: AbortSignal;
24
+ }
25
+ export interface StorageUploadResult {
26
+ /** The file ID (UUID) */
27
+ fileId: string;
28
+ /** The object key */
29
+ key: string;
30
+ /** Whether this file was deduplicated (no bytes uploaded) */
31
+ deduplicated: boolean;
32
+ /** Presigned URL expiry time (null if deduplicated) */
33
+ expiresAt: string | null;
34
+ /** ID of the previous version (when uploading a new version of a custom-keyed file) */
35
+ previousVersionId: string | null;
36
+ }
37
+ export interface UploadToSurfaceContext {
38
+ execute: GraphQLExecutor;
39
+ transport: StorageTransport;
40
+ }
41
+ export declare function uploadToSurface(surface: StorageSurface, options: StorageUploadOptions, context: UploadToSurfaceContext): Promise<StorageUploadResult>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Upload orchestration for one already-resolved storage plane:
4
+ * hash → dynamic upload mutation → presigned PUT.
5
+ *
6
+ * Standalone on purpose — callers that resolved a surface themselves can use
7
+ * this without the client wrapper, and the byte-level work is an injected
8
+ * `StorageTransport`.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.uploadToSurface = uploadToSurface;
12
+ const document_1 = require("./document");
13
+ const transport_1 = require("./transport");
14
+ async function uploadToSurface(surface, options, context) {
15
+ const { execute, transport } = context;
16
+ const { file, bucketKey, key, isPublic, ownerId, signal } = options;
17
+ if (!file) {
18
+ throw new transport_1.StorageError('INVALID_FILE', 'No file provided');
19
+ }
20
+ if (file.size <= 0) {
21
+ throw new transport_1.StorageError('INVALID_FILE', 'File is empty');
22
+ }
23
+ if (surface.upload.requiresOwnerId && !ownerId) {
24
+ throw new transport_1.StorageError('OWNER_REQUIRED', `Storage plane ${surface.filesType} is entity-keyed and requires ownerId`);
25
+ }
26
+ checkAborted(signal);
27
+ const contentHash = await transport.hashFile(file);
28
+ checkAborted(signal);
29
+ const contentType = file.type || 'application/octet-stream';
30
+ const input = {
31
+ contentHash,
32
+ contentType,
33
+ size: file.size,
34
+ filename: file.name || undefined,
35
+ };
36
+ if (bucketKey !== undefined)
37
+ input.bucketKey = bucketKey;
38
+ if (key !== undefined)
39
+ input.key = key;
40
+ if (isPublic !== undefined)
41
+ input.isPublic = isPublic;
42
+ if (ownerId !== undefined)
43
+ input.ownerId = ownerId;
44
+ let data;
45
+ try {
46
+ data = await execute((0, document_1.buildUploadDocument)(surface), { input });
47
+ }
48
+ catch (err) {
49
+ throw new transport_1.StorageError('UPLOAD_MUTATION_FAILED', `${surface.upload.mutation} mutation failed: ${err instanceof Error ? err.message : String(err)}`, err);
50
+ }
51
+ const payload = data?.[surface.upload.mutation];
52
+ if (!payload) {
53
+ throw new transport_1.StorageError('UPLOAD_MUTATION_FAILED', `No data returned from ${surface.upload.mutation}`);
54
+ }
55
+ if (payload.deduplicated) {
56
+ return toResult(payload);
57
+ }
58
+ if (!payload.uploadUrl) {
59
+ throw new transport_1.StorageError('UPLOAD_MUTATION_FAILED', 'Server returned deduplicated=false but no uploadUrl');
60
+ }
61
+ checkAborted(signal);
62
+ await transport.putObject(payload.uploadUrl, await file.arrayBuffer(), contentType, signal);
63
+ return toResult(payload);
64
+ }
65
+ function toResult(payload) {
66
+ return {
67
+ fileId: payload.fileId,
68
+ key: payload.key,
69
+ deduplicated: payload.deduplicated,
70
+ expiresAt: payload.expiresAt ?? null,
71
+ previousVersionId: payload.previousVersionId ?? null,
72
+ };
73
+ }
74
+ function checkAborted(signal) {
75
+ if (signal?.aborted) {
76
+ throw new transport_1.StorageError('ABORTED', 'Upload was cancelled');
77
+ }
78
+ }