@objectql/server 1.8.1 → 1.8.2

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,201 @@
1
+ /**
2
+ * Integration Test Example for File Upload/Download
3
+ *
4
+ * This example demonstrates how to use the file upload/download API
5
+ * with a complete ObjectQL server setup.
6
+ */
7
+
8
+ import { ObjectQL } from '@objectql/core';
9
+ import { SqlDriver } from '@objectql/driver-sql';
10
+ import { createNodeHandler, MemoryFileStorage } from '../src';
11
+ import * as http from 'http';
12
+
13
+ async function setupServer() {
14
+ // 1. Initialize Driver (In-Memory SQLite)
15
+ const driver = new SqlDriver({
16
+ client: 'sqlite3',
17
+ connection: { filename: ':memory:' },
18
+ useNullAsDefault: true
19
+ });
20
+
21
+ // 2. Initialize Engine
22
+ const app = new ObjectQL({
23
+ datasources: { default: driver }
24
+ });
25
+
26
+ // 3. Define Schema with File Attachment
27
+ app.registerObject({
28
+ name: 'expense',
29
+ label: 'Expense',
30
+ fields: {
31
+ expense_number: {
32
+ type: 'text',
33
+ required: true,
34
+ label: 'Expense Number'
35
+ },
36
+ amount: {
37
+ type: 'number',
38
+ required: true,
39
+ label: 'Amount'
40
+ },
41
+ description: {
42
+ type: 'textarea',
43
+ label: 'Description'
44
+ },
45
+ receipt: {
46
+ type: 'file',
47
+ label: 'Receipt',
48
+ accept: ['.pdf', '.jpg', '.jpeg', '.png'],
49
+ max_size: 5242880 // 5MB
50
+ }
51
+ }
52
+ });
53
+
54
+ await app.init();
55
+
56
+ // 4. Create HTTP Server with File Storage
57
+ const fileStorage = new MemoryFileStorage({
58
+ baseUrl: 'http://localhost:3000/api/files'
59
+ });
60
+
61
+ const handler = createNodeHandler(app, { fileStorage });
62
+ const server = http.createServer(handler);
63
+
64
+ return { server, app, fileStorage };
65
+ }
66
+
67
+ async function demonstrateUsage() {
68
+ console.log('=== ObjectQL File Upload/Download Integration Example ===\n');
69
+
70
+ const { server, app, fileStorage } = await setupServer();
71
+
72
+ // Start server
73
+ await new Promise<void>((resolve) => {
74
+ server.listen(3000, () => {
75
+ console.log('✓ Server started on http://localhost:3000');
76
+ resolve();
77
+ });
78
+ });
79
+
80
+ try {
81
+ // Example 1: Upload a file
82
+ console.log('\n1. Uploading a file...');
83
+ const fileContent = Buffer.from('Sample receipt content');
84
+ const uploadedFile = await fileStorage.save(
85
+ fileContent,
86
+ 'receipt.pdf',
87
+ 'application/pdf',
88
+ { userId: 'user_123' }
89
+ );
90
+ console.log('✓ File uploaded:', uploadedFile);
91
+
92
+ // Example 2: Create an expense with the uploaded file
93
+ console.log('\n2. Creating expense record with attachment...');
94
+ const ctx = app.createContext({ isSystem: true });
95
+ const expenseRepo = ctx.object('expense');
96
+
97
+ const expense = await expenseRepo.create({
98
+ expense_number: 'EXP-2024-001',
99
+ amount: 125.50,
100
+ description: 'Office supplies',
101
+ receipt: uploadedFile
102
+ });
103
+ console.log('✓ Expense created:', {
104
+ id: expense.id,
105
+ expense_number: expense.expense_number,
106
+ receipt: expense.receipt
107
+ });
108
+
109
+ // Example 3: Retrieve the file
110
+ console.log('\n3. Downloading the file...');
111
+ const downloadedFile = await fileStorage.get(uploadedFile.id!);
112
+ console.log('✓ File downloaded:', {
113
+ size: downloadedFile?.length,
114
+ content: downloadedFile?.toString().substring(0, 50) + '...'
115
+ });
116
+
117
+ // Example 4: Query expenses with attachments
118
+ console.log('\n4. Querying expenses with attachments...');
119
+ const expenses = await expenseRepo.find({
120
+ filters: [['receipt', '!=', null]]
121
+ });
122
+ console.log('✓ Found expenses with receipts:', expenses.length);
123
+ expenses.forEach((exp: any) => {
124
+ console.log(` - ${exp.expense_number}: ${exp.receipt?.name}`);
125
+ });
126
+
127
+ // Example 5: Update attachment
128
+ console.log('\n5. Uploading and updating receipt...');
129
+ const newFileContent = Buffer.from('Updated receipt content');
130
+ const newFile = await fileStorage.save(
131
+ newFileContent,
132
+ 'updated_receipt.pdf',
133
+ 'application/pdf',
134
+ { userId: 'user_123' }
135
+ );
136
+
137
+ await expenseRepo.update(expense.id, {
138
+ receipt: newFile
139
+ });
140
+ console.log('✓ Receipt updated');
141
+
142
+ // Example 6: Multiple file upload (product gallery)
143
+ console.log('\n6. Multiple file upload example...');
144
+ app.registerObject({
145
+ name: 'product',
146
+ label: 'Product',
147
+ fields: {
148
+ name: { type: 'text', required: true },
149
+ price: { type: 'number' },
150
+ gallery: {
151
+ type: 'image',
152
+ label: 'Product Gallery',
153
+ multiple: true,
154
+ accept: ['.jpg', '.jpeg', '.png'],
155
+ max_size: 2097152 // 2MB per image
156
+ }
157
+ }
158
+ });
159
+
160
+ const productRepo = ctx.object('product');
161
+
162
+ // Upload multiple images
163
+ const images = await Promise.all([
164
+ fileStorage.save(
165
+ Buffer.from('Image 1 content'),
166
+ 'product_1.jpg',
167
+ 'image/jpeg',
168
+ { userId: 'user_123' }
169
+ ),
170
+ fileStorage.save(
171
+ Buffer.from('Image 2 content'),
172
+ 'product_2.jpg',
173
+ 'image/jpeg',
174
+ { userId: 'user_123' }
175
+ )
176
+ ]);
177
+
178
+ const product = await productRepo.create({
179
+ name: 'Premium Laptop',
180
+ price: 1299.99,
181
+ gallery: images
182
+ });
183
+ console.log('✓ Product created with gallery:', {
184
+ id: product.id,
185
+ name: product.name,
186
+ images: product.gallery.length
187
+ });
188
+
189
+ } finally {
190
+ // Cleanup
191
+ server.close();
192
+ console.log('\n✓ Server stopped');
193
+ }
194
+ }
195
+
196
+ // Run the example
197
+ if (require.main === module) {
198
+ demonstrateUsage().catch(console.error);
199
+ }
200
+
201
+ export { setupServer, demonstrateUsage };
@@ -0,0 +1,131 @@
1
+ import { validateFile } from '../src/file-handler';
2
+ import { FieldConfig } from '@objectql/types';
3
+
4
+ describe('File Validation', () => {
5
+ describe('validateFile', () => {
6
+ it('should pass validation when no field config is provided', () => {
7
+ const file = {
8
+ filename: 'test.pdf',
9
+ mimeType: 'application/pdf',
10
+ buffer: Buffer.from('test content')
11
+ };
12
+
13
+ const result = validateFile(file);
14
+
15
+ expect(result.valid).toBe(true);
16
+ expect(result.error).toBeUndefined();
17
+ });
18
+
19
+ it('should validate file size limits', () => {
20
+ const file = {
21
+ filename: 'test.pdf',
22
+ mimeType: 'application/pdf',
23
+ buffer: Buffer.from('a'.repeat(1000)) // 1000 bytes
24
+ };
25
+
26
+ const fieldConfig: FieldConfig = {
27
+ type: 'file',
28
+ max_size: 500 // 500 bytes max
29
+ };
30
+
31
+ const result = validateFile(file, fieldConfig);
32
+
33
+ expect(result.valid).toBe(false);
34
+ expect(result.error?.code).toBe('FILE_TOO_LARGE');
35
+ expect(result.error?.message).toContain('exceeds maximum');
36
+ });
37
+
38
+ it('should validate minimum file size', () => {
39
+ const file = {
40
+ filename: 'test.pdf',
41
+ mimeType: 'application/pdf',
42
+ buffer: Buffer.from('small')
43
+ };
44
+
45
+ const fieldConfig: FieldConfig = {
46
+ type: 'file',
47
+ min_size: 100 // 100 bytes minimum
48
+ };
49
+
50
+ const result = validateFile(file, fieldConfig);
51
+
52
+ expect(result.valid).toBe(false);
53
+ expect(result.error?.code).toBe('FILE_TOO_SMALL');
54
+ expect(result.error?.message).toContain('below minimum');
55
+ });
56
+
57
+ it('should validate accepted file extensions', () => {
58
+ const file = {
59
+ filename: 'test.exe',
60
+ mimeType: 'application/x-msdownload',
61
+ buffer: Buffer.from('test content')
62
+ };
63
+
64
+ const fieldConfig: FieldConfig = {
65
+ type: 'file',
66
+ accept: ['.pdf', '.jpg', '.png']
67
+ };
68
+
69
+ const result = validateFile(file, fieldConfig);
70
+
71
+ expect(result.valid).toBe(false);
72
+ expect(result.error?.code).toBe('FILE_TYPE_NOT_ALLOWED');
73
+ expect(result.error?.message).toContain('not allowed');
74
+ });
75
+
76
+ it('should accept file when extension is in allowed list', () => {
77
+ const file = {
78
+ filename: 'document.pdf',
79
+ mimeType: 'application/pdf',
80
+ buffer: Buffer.from('test content')
81
+ };
82
+
83
+ const fieldConfig: FieldConfig = {
84
+ type: 'file',
85
+ accept: ['.pdf', '.docx']
86
+ };
87
+
88
+ const result = validateFile(file, fieldConfig);
89
+
90
+ expect(result.valid).toBe(true);
91
+ expect(result.error).toBeUndefined();
92
+ });
93
+
94
+ it('should handle case-insensitive extension matching', () => {
95
+ const file = {
96
+ filename: 'document.PDF',
97
+ mimeType: 'application/pdf',
98
+ buffer: Buffer.from('test content')
99
+ };
100
+
101
+ const fieldConfig: FieldConfig = {
102
+ type: 'file',
103
+ accept: ['.pdf']
104
+ };
105
+
106
+ const result = validateFile(file, fieldConfig);
107
+
108
+ expect(result.valid).toBe(true);
109
+ });
110
+
111
+ it('should pass all validations for valid file', () => {
112
+ const file = {
113
+ filename: 'receipt.jpg',
114
+ mimeType: 'image/jpeg',
115
+ buffer: Buffer.from('a'.repeat(500))
116
+ };
117
+
118
+ const fieldConfig: FieldConfig = {
119
+ type: 'image',
120
+ accept: ['.jpg', '.jpeg', '.png'],
121
+ max_size: 1000,
122
+ min_size: 100
123
+ };
124
+
125
+ const result = validateFile(file, fieldConfig);
126
+
127
+ expect(result.valid).toBe(true);
128
+ expect(result.error).toBeUndefined();
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,104 @@
1
+ import { MemoryFileStorage } from '../src/storage';
2
+ import { AttachmentData } from '../src/types';
3
+
4
+ describe('MemoryFileStorage', () => {
5
+ let storage: MemoryFileStorage;
6
+
7
+ beforeEach(() => {
8
+ storage = new MemoryFileStorage({
9
+ baseUrl: 'http://localhost:3000/api/files'
10
+ });
11
+ });
12
+
13
+ afterEach(() => {
14
+ storage.clear();
15
+ });
16
+
17
+ describe('save', () => {
18
+ it('should save a file and return attachment metadata', async () => {
19
+ const fileBuffer = Buffer.from('test file content');
20
+ const filename = 'test.txt';
21
+ const mimeType = 'text/plain';
22
+
23
+ const result = await storage.save(fileBuffer, filename, mimeType);
24
+
25
+ expect(result).toHaveProperty('id');
26
+ expect(result).toHaveProperty('name');
27
+ expect(result.url).toContain('http://localhost:3000/api/files');
28
+ expect(result.size).toBe(fileBuffer.length);
29
+ expect(result.type).toBe(mimeType);
30
+ expect(result.original_name).toBe(filename);
31
+ expect(result).toHaveProperty('uploaded_at');
32
+ });
33
+
34
+ it('should include user ID when provided', async () => {
35
+ const fileBuffer = Buffer.from('test file content');
36
+ const filename = 'test.txt';
37
+ const mimeType = 'text/plain';
38
+ const userId = 'user_123';
39
+
40
+ const result = await storage.save(fileBuffer, filename, mimeType, { userId });
41
+
42
+ expect(result.uploaded_by).toBe(userId);
43
+ });
44
+
45
+ it('should preserve file extension in stored filename', async () => {
46
+ const fileBuffer = Buffer.from('test file content');
47
+ const filename = 'document.pdf';
48
+ const mimeType = 'application/pdf';
49
+
50
+ const result = await storage.save(fileBuffer, filename, mimeType);
51
+
52
+ expect(result.name).toMatch(/\.pdf$/);
53
+ });
54
+ });
55
+
56
+ describe('get', () => {
57
+ it('should retrieve a saved file', async () => {
58
+ const fileBuffer = Buffer.from('test file content');
59
+ const filename = 'test.txt';
60
+ const mimeType = 'text/plain';
61
+
62
+ const attachment = await storage.save(fileBuffer, filename, mimeType);
63
+ const retrieved = await storage.get(attachment.id!);
64
+
65
+ expect(retrieved).not.toBeNull();
66
+ expect(retrieved!.toString()).toBe(fileBuffer.toString());
67
+ });
68
+
69
+ it('should return null for non-existent file', async () => {
70
+ const result = await storage.get('non_existent_id');
71
+ expect(result).toBeNull();
72
+ });
73
+ });
74
+
75
+ describe('delete', () => {
76
+ it('should delete a file', async () => {
77
+ const fileBuffer = Buffer.from('test file content');
78
+ const filename = 'test.txt';
79
+ const mimeType = 'text/plain';
80
+
81
+ const attachment = await storage.save(fileBuffer, filename, mimeType);
82
+ const deleted = await storage.delete(attachment.id!);
83
+
84
+ expect(deleted).toBe(true);
85
+
86
+ const retrieved = await storage.get(attachment.id!);
87
+ expect(retrieved).toBeNull();
88
+ });
89
+
90
+ it('should return false when deleting non-existent file', async () => {
91
+ const result = await storage.delete('non_existent_id');
92
+ expect(result).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe('getPublicUrl', () => {
97
+ it('should generate a public URL', () => {
98
+ const filePath = 'uploads/test.txt';
99
+ const url = storage.getPublicUrl(filePath);
100
+
101
+ expect(url).toBe('http://localhost:3000/api/files/uploads/test.txt');
102
+ });
103
+ });
104
+ });