@objectql/server 1.8.1 → 1.8.3

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,181 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MemoryFileStorage = exports.LocalFileStorage = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto = __importStar(require("crypto"));
40
+ /**
41
+ * Local filesystem storage implementation for file attachments
42
+ */
43
+ class LocalFileStorage {
44
+ constructor(options) {
45
+ this.baseDir = options.baseDir;
46
+ this.baseUrl = options.baseUrl;
47
+ // Ensure base directory exists
48
+ if (!fs.existsSync(this.baseDir)) {
49
+ fs.mkdirSync(this.baseDir, { recursive: true });
50
+ }
51
+ }
52
+ async save(file, filename, mimeType, options) {
53
+ // Generate unique ID for the file
54
+ const id = crypto.randomBytes(16).toString('hex');
55
+ const ext = path.extname(filename);
56
+ const basename = path.basename(filename, ext);
57
+ const storedFilename = `${id}${ext}`;
58
+ // Determine storage path
59
+ let folder = (options === null || options === void 0 ? void 0 : options.folder) || 'uploads';
60
+ if (options === null || options === void 0 ? void 0 : options.object) {
61
+ folder = path.join(folder, options.object);
62
+ }
63
+ const folderPath = path.join(this.baseDir, folder);
64
+ if (!fs.existsSync(folderPath)) {
65
+ fs.mkdirSync(folderPath, { recursive: true });
66
+ }
67
+ const filePath = path.join(folderPath, storedFilename);
68
+ // Write file to disk (async for better performance)
69
+ await fs.promises.writeFile(filePath, file);
70
+ // Generate public URL
71
+ const url = this.getPublicUrl(path.join(folder, storedFilename));
72
+ const attachmentData = {
73
+ id,
74
+ name: storedFilename,
75
+ url,
76
+ size: file.length,
77
+ type: mimeType,
78
+ original_name: filename,
79
+ uploaded_at: new Date().toISOString(),
80
+ uploaded_by: options === null || options === void 0 ? void 0 : options.userId
81
+ };
82
+ return attachmentData;
83
+ }
84
+ async get(fileId) {
85
+ try {
86
+ // Search for file in the upload directory
87
+ const found = this.findFile(this.baseDir, fileId);
88
+ if (!found) {
89
+ return null;
90
+ }
91
+ // Use async read for better performance
92
+ return await fs.promises.readFile(found);
93
+ }
94
+ catch (error) {
95
+ console.error('Error reading file:', error);
96
+ return null;
97
+ }
98
+ }
99
+ async delete(fileId) {
100
+ try {
101
+ const found = this.findFile(this.baseDir, fileId);
102
+ if (!found) {
103
+ return false;
104
+ }
105
+ // Use async unlink for better performance
106
+ await fs.promises.unlink(found);
107
+ return true;
108
+ }
109
+ catch (error) {
110
+ console.error('Error deleting file:', error);
111
+ return false;
112
+ }
113
+ }
114
+ getPublicUrl(filePath) {
115
+ // Normalize path separators for URLs
116
+ const normalizedPath = filePath.replace(/\\/g, '/');
117
+ return `${this.baseUrl}/${normalizedPath}`;
118
+ }
119
+ /**
120
+ * Recursively search for a file by ID
121
+ */
122
+ findFile(dir, fileId) {
123
+ const files = fs.readdirSync(dir);
124
+ for (const file of files) {
125
+ const filePath = path.join(dir, file);
126
+ const stat = fs.statSync(filePath);
127
+ if (stat.isDirectory()) {
128
+ const found = this.findFile(filePath, fileId);
129
+ if (found) {
130
+ return found;
131
+ }
132
+ }
133
+ else if (file.startsWith(fileId)) {
134
+ return filePath;
135
+ }
136
+ }
137
+ return null;
138
+ }
139
+ }
140
+ exports.LocalFileStorage = LocalFileStorage;
141
+ /**
142
+ * Memory storage implementation for testing
143
+ */
144
+ class MemoryFileStorage {
145
+ constructor(options) {
146
+ this.files = new Map();
147
+ this.baseUrl = options.baseUrl;
148
+ }
149
+ async save(file, filename, mimeType, options) {
150
+ const id = crypto.randomBytes(16).toString('hex');
151
+ const ext = path.extname(filename);
152
+ const storedFilename = `${id}${ext}`;
153
+ const attachmentData = {
154
+ id,
155
+ name: storedFilename,
156
+ url: this.getPublicUrl(storedFilename),
157
+ size: file.length,
158
+ type: mimeType,
159
+ original_name: filename,
160
+ uploaded_at: new Date().toISOString(),
161
+ uploaded_by: options === null || options === void 0 ? void 0 : options.userId
162
+ };
163
+ this.files.set(id, { buffer: file, metadata: attachmentData });
164
+ return attachmentData;
165
+ }
166
+ async get(fileId) {
167
+ const entry = this.files.get(fileId);
168
+ return entry ? entry.buffer : null;
169
+ }
170
+ async delete(fileId) {
171
+ return this.files.delete(fileId);
172
+ }
173
+ getPublicUrl(filePath) {
174
+ return `${this.baseUrl}/${filePath}`;
175
+ }
176
+ clear() {
177
+ this.files.clear();
178
+ }
179
+ }
180
+ exports.MemoryFileStorage = MemoryFileStorage;
181
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -70,3 +70,75 @@ export interface ObjectQLResponse {
70
70
  };
71
71
  [key: string]: any;
72
72
  }
73
+ /**
74
+ * Attachment/File metadata structure
75
+ */
76
+ export interface AttachmentData {
77
+ /** Unique identifier for this file */
78
+ id: string;
79
+ /** File name (e.g., "invoice.pdf") */
80
+ name: string;
81
+ /** Publicly accessible URL to the file */
82
+ url: string;
83
+ /** File size in bytes */
84
+ size: number;
85
+ /** MIME type (e.g., "application/pdf", "image/jpeg") */
86
+ type: string;
87
+ /** Original filename as uploaded by user */
88
+ original_name?: string;
89
+ /** Upload timestamp (ISO 8601) */
90
+ uploaded_at?: string;
91
+ /** User ID who uploaded the file */
92
+ uploaded_by?: string;
93
+ }
94
+ /**
95
+ * Image-specific attachment data with metadata
96
+ */
97
+ export interface ImageAttachmentData extends AttachmentData {
98
+ /** Image width in pixels */
99
+ width?: number;
100
+ /** Image height in pixels */
101
+ height?: number;
102
+ /** Thumbnail URL (if generated) */
103
+ thumbnail_url?: string;
104
+ /** Alternative sizes/versions */
105
+ variants?: {
106
+ small?: string;
107
+ medium?: string;
108
+ large?: string;
109
+ };
110
+ }
111
+ /**
112
+ * File storage provider interface
113
+ */
114
+ export interface IFileStorage {
115
+ /**
116
+ * Save a file and return its metadata
117
+ */
118
+ save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise<AttachmentData>;
119
+ /**
120
+ * Retrieve a file by its ID or path
121
+ */
122
+ get(fileId: string): Promise<Buffer | null>;
123
+ /**
124
+ * Delete a file
125
+ */
126
+ delete(fileId: string): Promise<boolean>;
127
+ /**
128
+ * Generate a public URL for a file
129
+ */
130
+ getPublicUrl(fileId: string): string;
131
+ }
132
+ /**
133
+ * Options for file storage operations
134
+ */
135
+ export interface FileStorageOptions {
136
+ /** Logical folder/path for organization */
137
+ folder?: string;
138
+ /** Object name (for context/validation) */
139
+ object?: string;
140
+ /** Field name (for validation against field config) */
141
+ field?: string;
142
+ /** User ID who uploaded the file */
143
+ userId?: string;
144
+ }
package/package.json CHANGED
@@ -1,16 +1,28 @@
1
1
  {
2
2
  "name": "@objectql/server",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
+ "description": "HTTP server adapter for ObjectQL - Express/NestJS compatible with GraphQL and REST API support",
5
+ "keywords": [
6
+ "objectql",
7
+ "server",
8
+ "http",
9
+ "api",
10
+ "rest",
11
+ "graphql",
12
+ "express",
13
+ "nestjs",
14
+ "adapter",
15
+ "backend"
16
+ ],
4
17
  "license": "MIT",
5
- "description": "HTTP Server Adapter for ObjectQL",
6
18
  "main": "dist/index.js",
7
19
  "types": "dist/index.d.ts",
8
20
  "dependencies": {
9
21
  "graphql": "^16.8.1",
10
22
  "@graphql-tools/schema": "^10.0.2",
11
23
  "js-yaml": "^4.1.1",
12
- "@objectql/core": "1.8.1",
13
- "@objectql/types": "1.8.1"
24
+ "@objectql/core": "1.8.3",
25
+ "@objectql/types": "1.8.3"
14
26
  },
15
27
  "devDependencies": {
16
28
  "@types/js-yaml": "^4.0.9",
@@ -1,14 +1,35 @@
1
1
  import { IObjectQL } from '@objectql/types';
2
2
  import { ObjectQLServer } from '../server';
3
- import { ObjectQLRequest, ErrorCode } from '../types';
3
+ import { ObjectQLRequest, ErrorCode, IFileStorage } from '../types';
4
4
  import { IncomingMessage, ServerResponse } from 'http';
5
5
  import { generateOpenAPI } from '../openapi';
6
+ import { createFileUploadHandler, createBatchFileUploadHandler, createFileDownloadHandler } from '../file-handler';
7
+ import { LocalFileStorage } from '../storage';
8
+
9
+ /**
10
+ * Options for createNodeHandler
11
+ */
12
+ export interface NodeHandlerOptions {
13
+ /** File storage provider (defaults to LocalFileStorage) */
14
+ fileStorage?: IFileStorage;
15
+ }
6
16
 
7
17
  /**
8
18
  * Creates a standard Node.js HTTP request handler.
9
19
  */
10
- export function createNodeHandler(app: IObjectQL) {
20
+ export function createNodeHandler(app: IObjectQL, options?: NodeHandlerOptions) {
11
21
  const server = new ObjectQLServer(app);
22
+
23
+ // Initialize file storage
24
+ const fileStorage = options?.fileStorage || new LocalFileStorage({
25
+ baseDir: process.env.OBJECTQL_UPLOAD_DIR || './uploads',
26
+ baseUrl: process.env.OBJECTQL_BASE_URL || 'http://localhost:3000/api/files'
27
+ });
28
+
29
+ // Create file handlers
30
+ const uploadHandler = createFileUploadHandler(fileStorage, app);
31
+ const batchUploadHandler = createBatchFileUploadHandler(fileStorage, app);
32
+ const downloadHandler = createFileDownloadHandler(fileStorage);
12
33
 
13
34
 
14
35
  return async (req: IncomingMessage & { body?: any }, res: ServerResponse) => {
@@ -165,6 +186,27 @@ export function createNodeHandler(app: IObjectQL) {
165
186
  }
166
187
  }
167
188
 
189
+ // File Upload Endpoints
190
+ // POST /api/files/upload - Single file upload
191
+ if (pathName === '/api/files/upload' && method === 'POST') {
192
+ await uploadHandler(req, res);
193
+ return;
194
+ }
195
+
196
+ // POST /api/files/upload/batch - Batch file upload
197
+ if (pathName === '/api/files/upload/batch' && method === 'POST') {
198
+ await batchUploadHandler(req, res);
199
+ return;
200
+ }
201
+
202
+ // GET /api/files/:fileId - Download file
203
+ const fileMatch = pathName.match(/^\/api\/files\/([^/]+)$/);
204
+ if (fileMatch && method === 'GET') {
205
+ const fileId = fileMatch[1];
206
+ await downloadHandler(req, res, fileId);
207
+ return;
208
+ }
209
+
168
210
  // Fallback or 404
169
211
  if (req.method === 'POST') {
170
212
  // Fallback for root POSTs if people forget /api/objectql but send to /api something