s3db.js 8.2.0 → 9.1.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/README.md +14 -10
- package/dist/s3db-cli.js +54741 -0
- package/dist/s3db.cjs.js +434 -5671
- package/dist/s3db.cjs.js.map +1 -0
- package/dist/s3db.es.js +426 -5666
- package/dist/s3db.es.js.map +1 -0
- package/package.json +45 -29
- package/src/cli/index.js +426 -0
- package/src/client.class.js +8 -33
- package/src/concerns/advanced-metadata-encoding.js +440 -0
- package/src/concerns/calculator.js +36 -0
- package/src/concerns/metadata-encoding.js +244 -0
- package/src/concerns/optimized-encoding.js +130 -0
- package/dist/s3db.cjs.min.js +0 -1
- package/dist/s3db.es.min.js +0 -1
- package/dist/s3db.iife.js +0 -15738
- package/dist/s3db.iife.min.js +0 -1
package/package.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3db.js",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
|
|
5
5
|
"main": "dist/s3db.cjs.js",
|
|
6
6
|
"module": "dist/s3db.es.js",
|
|
7
|
-
"browser": "dist/s3db.iife.js",
|
|
8
7
|
"types": "dist/s3db.d.ts",
|
|
9
|
-
"unpkg": "dist/s3db.iife.min.js",
|
|
10
|
-
"jsdelivr": "dist/s3db.iife.min.js",
|
|
11
8
|
"author": "@stone/martech",
|
|
12
9
|
"license": "UNLICENSED",
|
|
13
10
|
"bin": {
|
|
@@ -26,7 +23,14 @@
|
|
|
26
23
|
"keywords": [
|
|
27
24
|
"s3",
|
|
28
25
|
"aws",
|
|
29
|
-
"database"
|
|
26
|
+
"database",
|
|
27
|
+
"orm",
|
|
28
|
+
"nosql",
|
|
29
|
+
"document-store",
|
|
30
|
+
"cloud-database",
|
|
31
|
+
"metadata-encoding",
|
|
32
|
+
"s3-database",
|
|
33
|
+
"serverless"
|
|
30
34
|
],
|
|
31
35
|
"type": "module",
|
|
32
36
|
"sideEffects": false,
|
|
@@ -39,24 +43,25 @@
|
|
|
39
43
|
},
|
|
40
44
|
"exports": {
|
|
41
45
|
".": {
|
|
46
|
+
"types": "./dist/s3db.d.ts",
|
|
42
47
|
"import": "./dist/s3db.es.js",
|
|
43
|
-
"require": "./dist/s3db.cjs.js"
|
|
44
|
-
"types": "./dist/s3db.d.ts"
|
|
48
|
+
"require": "./dist/s3db.cjs.js"
|
|
45
49
|
}
|
|
46
50
|
},
|
|
47
51
|
"files": [
|
|
48
|
-
"dist",
|
|
49
|
-
"src",
|
|
52
|
+
"dist/",
|
|
53
|
+
"src/",
|
|
54
|
+
"bin/cli.js",
|
|
55
|
+
"mcp/server.js",
|
|
50
56
|
"README.md",
|
|
51
57
|
"PLUGINS.md",
|
|
52
58
|
"UNLICENSE"
|
|
53
59
|
],
|
|
54
60
|
"dependencies": {
|
|
55
|
-
"@aws-sdk/client-s3": "^3.
|
|
61
|
+
"@aws-sdk/client-s3": "^3.864.0",
|
|
56
62
|
"@modelcontextprotocol/sdk": "^1.17.3",
|
|
57
|
-
"@smithy/node-http-handler": "^4.1.
|
|
63
|
+
"@smithy/node-http-handler": "^4.1.1",
|
|
58
64
|
"@supercharge/promise-pool": "^3.2.0",
|
|
59
|
-
"commander": "^12.1.0",
|
|
60
65
|
"dotenv": "^17.2.1",
|
|
61
66
|
"fastest-validator": "^1.19.1",
|
|
62
67
|
"flat": "^6.0.1",
|
|
@@ -89,43 +94,54 @@
|
|
|
89
94
|
}
|
|
90
95
|
},
|
|
91
96
|
"devDependencies": {
|
|
97
|
+
"@babel/core": "^7.28.3",
|
|
98
|
+
"@babel/preset-env": "^7.28.3",
|
|
92
99
|
"@rollup/plugin-commonjs": "^28.0.6",
|
|
93
100
|
"@rollup/plugin-json": "^6.1.0",
|
|
94
101
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
95
102
|
"@rollup/plugin-replace": "^6.0.2",
|
|
96
103
|
"@rollup/plugin-terser": "^0.4.4",
|
|
97
|
-
"@types/node": "24.
|
|
104
|
+
"@types/node": "24.3.0",
|
|
105
|
+
"babel-loader": "^10.0.0",
|
|
106
|
+
"chalk": "^5.5.0",
|
|
107
|
+
"cli-table3": "^0.6.5",
|
|
108
|
+
"commander": "^14.0.0",
|
|
109
|
+
"esbuild": "^0.25.9",
|
|
110
|
+
"inquirer": "^12.9.2",
|
|
98
111
|
"jest": "^30.0.5",
|
|
99
|
-
"
|
|
112
|
+
"node-loader": "^2.1.0",
|
|
113
|
+
"ora": "^8.2.0",
|
|
114
|
+
"pkg": "^5.8.1",
|
|
115
|
+
"rollup": "^4.46.2",
|
|
100
116
|
"rollup-plugin-copy": "^3.5.0",
|
|
101
117
|
"rollup-plugin-esbuild": "^6.2.1",
|
|
102
118
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
|
119
|
+
"rollup-plugin-shebang-bin": "^0.1.0",
|
|
103
120
|
"rollup-plugin-terser": "^7.0.2",
|
|
104
|
-
"typescript": "5.
|
|
121
|
+
"typescript": "5.9.2",
|
|
122
|
+
"webpack": "^5.101.2",
|
|
123
|
+
"webpack-cli": "^6.0.1"
|
|
105
124
|
},
|
|
106
125
|
"funding": [
|
|
107
126
|
"https://github.com/sponsors/forattini-dev"
|
|
108
127
|
],
|
|
109
128
|
"scripts": {
|
|
110
129
|
"build": "rollup -c",
|
|
130
|
+
"build:cli": "rollup -c rollup.cli.config.mjs",
|
|
131
|
+
"build:binaries": "./scripts/scripts/build-binaries.sh",
|
|
111
132
|
"dev": "rollup -c -w",
|
|
112
|
-
"test": "
|
|
133
|
+
"test": "pnpm run test:js && pnpm run test:ts",
|
|
113
134
|
"test:js": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=10000",
|
|
114
135
|
"test:ts": "tsc --noEmit --project tests/typescript/tsconfig.json",
|
|
115
|
-
"test:
|
|
116
|
-
"test:js-ai": "node --max-old-space-size=4096 --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testPathIgnorePatterns=\"plugin-audit.test.js|tests/typescript/\"",
|
|
117
|
-
"test:full": "npm run test:js-ai && npm run test:ts",
|
|
118
|
-
"test:audit": "node --max-old-space-size=8192 --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-audit.test.js --runInBand --testTimeout=60000",
|
|
119
|
-
"test:cache": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/plugin-cache*.test.js --runInBand",
|
|
136
|
+
"test:coverage": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --coverage --runInBand",
|
|
120
137
|
"test:quick": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --testTimeout=10000",
|
|
121
|
-
"test:batch": "./test-batch.sh",
|
|
122
138
|
"test:plugins": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js tests/plugins/ --runInBand --testTimeout=60000",
|
|
123
|
-
"test:
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"validate:types": "
|
|
139
|
+
"test:full": "pnpm run test:js && pnpm run test:ts",
|
|
140
|
+
"benchmark": "node benchmark-compression.js",
|
|
141
|
+
"version": "echo 'Use pnpm run release v<version> instead of npm version'",
|
|
142
|
+
"release:check": "./scripts/pre-release-check.sh",
|
|
143
|
+
"release:prepare": "pnpm run build:binaries && echo 'Binaries ready for GitHub release'",
|
|
144
|
+
"release": "./scripts/release.sh",
|
|
145
|
+
"validate:types": "pnpm run test:ts && echo 'TypeScript definitions are valid!'"
|
|
130
146
|
}
|
|
131
147
|
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import Table from 'cli-table3';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import { S3db } from '../database.class.js';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
const configPath = path.join(os.homedir(), '.s3db', 'config.json');
|
|
15
|
+
|
|
16
|
+
// Helper to load config
|
|
17
|
+
async function loadConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const data = await fs.readFile(configPath, 'utf-8');
|
|
20
|
+
return JSON.parse(data);
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper to save config
|
|
27
|
+
async function saveConfig(config) {
|
|
28
|
+
const dir = path.dirname(configPath);
|
|
29
|
+
await fs.mkdir(dir, { recursive: true });
|
|
30
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Connect to database
|
|
34
|
+
async function getDatabase(options) {
|
|
35
|
+
const config = await loadConfig();
|
|
36
|
+
const connectionString = options.connection || config.connection || process.env.S3DB_CONNECTION;
|
|
37
|
+
|
|
38
|
+
if (!connectionString) {
|
|
39
|
+
console.error(chalk.red('No connection string provided. Use --connection or s3db configure'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new S3db({ connectionString });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.name('s3db')
|
|
48
|
+
.description('S3DB CLI - Transform AWS S3 into a powerful document database')
|
|
49
|
+
.version('9.0.0');
|
|
50
|
+
|
|
51
|
+
// Configure command
|
|
52
|
+
program
|
|
53
|
+
.command('configure')
|
|
54
|
+
.description('Configure S3DB connection')
|
|
55
|
+
.action(async () => {
|
|
56
|
+
const answers = await inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: 'input',
|
|
59
|
+
name: 'connection',
|
|
60
|
+
message: 'Enter S3 connection string:',
|
|
61
|
+
default: 's3://KEY:SECRET@bucket/database'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'list',
|
|
65
|
+
name: 'defaultBehavior',
|
|
66
|
+
message: 'Default behavior for resources:',
|
|
67
|
+
choices: ['user-managed', 'enforce-limits', 'body-overflow', 'body-only', 'truncate-data'],
|
|
68
|
+
default: 'user-managed'
|
|
69
|
+
}
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
await saveConfig(answers);
|
|
73
|
+
console.log(chalk.green('✓ Configuration saved to ~/.s3db/config.json'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// List resources
|
|
77
|
+
program
|
|
78
|
+
.command('list')
|
|
79
|
+
.description('List all resources')
|
|
80
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
81
|
+
.action(async (options) => {
|
|
82
|
+
const spinner = ora('Connecting to S3DB...').start();
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const db = await getDatabase(options);
|
|
86
|
+
await db.init();
|
|
87
|
+
|
|
88
|
+
const resources = await db.listResources();
|
|
89
|
+
spinner.stop();
|
|
90
|
+
|
|
91
|
+
if (resources.length === 0) {
|
|
92
|
+
console.log(chalk.yellow('No resources found'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const table = new Table({
|
|
97
|
+
head: ['Resource', 'Behavior', 'Timestamps', 'Paranoid', 'Partitions'],
|
|
98
|
+
style: { head: ['cyan'] }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
resources.forEach(r => {
|
|
102
|
+
table.push([
|
|
103
|
+
r.name,
|
|
104
|
+
r.config.behavior || 'user-managed',
|
|
105
|
+
r.config.timestamps ? '✓' : '✗',
|
|
106
|
+
r.config.paranoid ? '✓' : '✗',
|
|
107
|
+
Object.keys(r.config.partitions || {}).length
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(table.toString());
|
|
112
|
+
} catch (error) {
|
|
113
|
+
spinner.fail(chalk.red(error.message));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Query resource
|
|
119
|
+
program
|
|
120
|
+
.command('query <resource>')
|
|
121
|
+
.description('Query a resource')
|
|
122
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
123
|
+
.option('-l, --limit <number>', 'Limit results', '10')
|
|
124
|
+
.option('-f, --filter <json>', 'Filter as JSON')
|
|
125
|
+
.option('-p, --partition <name>', 'Partition name')
|
|
126
|
+
.option('--csv', 'Output as CSV')
|
|
127
|
+
.option('--json', 'Output as JSON')
|
|
128
|
+
.action(async (resourceName, options) => {
|
|
129
|
+
const spinner = ora('Querying...').start();
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const db = await getDatabase(options);
|
|
133
|
+
await db.init();
|
|
134
|
+
|
|
135
|
+
const resource = await db.resource(resourceName);
|
|
136
|
+
|
|
137
|
+
const queryOptions = {
|
|
138
|
+
limit: parseInt(options.limit)
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (options.filter) {
|
|
142
|
+
queryOptions.filter = JSON.parse(options.filter);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.partition) {
|
|
146
|
+
queryOptions.partition = options.partition;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const results = await resource.list(queryOptions);
|
|
150
|
+
spinner.stop();
|
|
151
|
+
|
|
152
|
+
if (options.json) {
|
|
153
|
+
console.log(JSON.stringify(results, null, 2));
|
|
154
|
+
} else if (options.csv) {
|
|
155
|
+
if (results.length > 0) {
|
|
156
|
+
const headers = Object.keys(results[0]);
|
|
157
|
+
console.log(headers.join(','));
|
|
158
|
+
results.forEach(row => {
|
|
159
|
+
console.log(headers.map(h => JSON.stringify(row[h] || '')).join(','));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// Table output
|
|
164
|
+
if (results.length === 0) {
|
|
165
|
+
console.log(chalk.yellow('No results found'));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const headers = Object.keys(results[0]);
|
|
170
|
+
const table = new Table({
|
|
171
|
+
head: headers,
|
|
172
|
+
style: { head: ['cyan'] }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
results.forEach(row => {
|
|
176
|
+
table.push(headers.map(h => {
|
|
177
|
+
const val = row[h];
|
|
178
|
+
if (val === null || val === undefined) return '';
|
|
179
|
+
if (typeof val === 'object') return JSON.stringify(val);
|
|
180
|
+
return String(val).substring(0, 50);
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(table.toString());
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
spinner.fail(chalk.red(error.message));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Insert data
|
|
193
|
+
program
|
|
194
|
+
.command('insert <resource>')
|
|
195
|
+
.description('Insert data into a resource')
|
|
196
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
197
|
+
.option('-d, --data <json>', 'Data as JSON')
|
|
198
|
+
.option('-f, --file <path>', 'Read data from file')
|
|
199
|
+
.action(async (resourceName, options) => {
|
|
200
|
+
const spinner = ora('Inserting...').start();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const db = await getDatabase(options);
|
|
204
|
+
await db.init();
|
|
205
|
+
|
|
206
|
+
const resource = await db.resource(resourceName);
|
|
207
|
+
|
|
208
|
+
let data;
|
|
209
|
+
if (options.file) {
|
|
210
|
+
const content = await fs.readFile(options.file, 'utf-8');
|
|
211
|
+
data = JSON.parse(content);
|
|
212
|
+
} else if (options.data) {
|
|
213
|
+
data = JSON.parse(options.data);
|
|
214
|
+
} else {
|
|
215
|
+
spinner.fail('No data provided. Use --data or --file');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = await resource.insert(data);
|
|
220
|
+
spinner.succeed(chalk.green(`✓ Inserted with ID: ${result.id}`));
|
|
221
|
+
|
|
222
|
+
if (!options.quiet) {
|
|
223
|
+
console.log(JSON.stringify(result, null, 2));
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
spinner.fail(chalk.red(error.message));
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Update data
|
|
232
|
+
program
|
|
233
|
+
.command('update <resource> <id>')
|
|
234
|
+
.description('Update a record')
|
|
235
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
236
|
+
.option('-d, --data <json>', 'Data as JSON')
|
|
237
|
+
.action(async (resourceName, id, options) => {
|
|
238
|
+
const spinner = ora('Updating...').start();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const db = await getDatabase(options);
|
|
242
|
+
await db.init();
|
|
243
|
+
|
|
244
|
+
const resource = await db.resource(resourceName);
|
|
245
|
+
const data = JSON.parse(options.data || '{}');
|
|
246
|
+
|
|
247
|
+
const result = await resource.update(id, data);
|
|
248
|
+
spinner.succeed(chalk.green(`✓ Updated ID: ${id}`));
|
|
249
|
+
|
|
250
|
+
console.log(JSON.stringify(result, null, 2));
|
|
251
|
+
} catch (error) {
|
|
252
|
+
spinner.fail(chalk.red(error.message));
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Delete data
|
|
258
|
+
program
|
|
259
|
+
.command('delete <resource> <id>')
|
|
260
|
+
.description('Delete a record')
|
|
261
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
262
|
+
.option('--force', 'Force delete (no confirmation)')
|
|
263
|
+
.action(async (resourceName, id, options) => {
|
|
264
|
+
if (!options.force) {
|
|
265
|
+
const { confirm } = await inquirer.prompt([
|
|
266
|
+
{
|
|
267
|
+
type: 'confirm',
|
|
268
|
+
name: 'confirm',
|
|
269
|
+
message: `Are you sure you want to delete ${id} from ${resourceName}?`,
|
|
270
|
+
default: false
|
|
271
|
+
}
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
if (!confirm) {
|
|
275
|
+
console.log(chalk.yellow('Cancelled'));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const spinner = ora('Deleting...').start();
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const db = await getDatabase(options);
|
|
284
|
+
await db.init();
|
|
285
|
+
|
|
286
|
+
const resource = await db.resource(resourceName);
|
|
287
|
+
await resource.delete(id);
|
|
288
|
+
|
|
289
|
+
spinner.succeed(chalk.green(`✓ Deleted ID: ${id}`));
|
|
290
|
+
} catch (error) {
|
|
291
|
+
spinner.fail(chalk.red(error.message));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Create resource
|
|
297
|
+
program
|
|
298
|
+
.command('create-resource <name>')
|
|
299
|
+
.description('Create a new resource')
|
|
300
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
301
|
+
.option('-s, --schema <json>', 'Schema as JSON')
|
|
302
|
+
.option('-b, --behavior <type>', 'Behavior type', 'user-managed')
|
|
303
|
+
.option('--timestamps', 'Enable timestamps')
|
|
304
|
+
.option('--paranoid', 'Enable soft deletes')
|
|
305
|
+
.action(async (name, options) => {
|
|
306
|
+
const spinner = ora('Creating resource...').start();
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const db = await getDatabase(options);
|
|
310
|
+
await db.init();
|
|
311
|
+
|
|
312
|
+
const config = {
|
|
313
|
+
name,
|
|
314
|
+
behavior: options.behavior,
|
|
315
|
+
timestamps: options.timestamps,
|
|
316
|
+
paranoid: options.paranoid
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
if (options.schema) {
|
|
320
|
+
config.attributes = JSON.parse(options.schema);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const resource = await db.createResource(config);
|
|
324
|
+
spinner.succeed(chalk.green(`✓ Created resource: ${name}`));
|
|
325
|
+
|
|
326
|
+
console.log(JSON.stringify(resource.config, null, 2));
|
|
327
|
+
} catch (error) {
|
|
328
|
+
spinner.fail(chalk.red(error.message));
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Interactive mode
|
|
334
|
+
program
|
|
335
|
+
.command('interactive')
|
|
336
|
+
.description('Interactive REPL mode')
|
|
337
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
338
|
+
.action(async (options) => {
|
|
339
|
+
console.log(chalk.cyan('S3DB Interactive Mode'));
|
|
340
|
+
console.log(chalk.gray('Type "help" for commands, "exit" to quit\n'));
|
|
341
|
+
|
|
342
|
+
const db = await getDatabase(options);
|
|
343
|
+
await db.init();
|
|
344
|
+
|
|
345
|
+
const repl = await import('repl');
|
|
346
|
+
const server = repl.start({
|
|
347
|
+
prompt: chalk.green('s3db> '),
|
|
348
|
+
eval: async (cmd, context, filename, callback) => {
|
|
349
|
+
try {
|
|
350
|
+
// Make db available in REPL
|
|
351
|
+
context.db = db;
|
|
352
|
+
|
|
353
|
+
// Parse commands
|
|
354
|
+
const trimmed = cmd.trim();
|
|
355
|
+
if (trimmed === 'help') {
|
|
356
|
+
console.log(`
|
|
357
|
+
Available commands:
|
|
358
|
+
db - Database instance
|
|
359
|
+
db.listResources() - List all resources
|
|
360
|
+
db.resource('name') - Get a resource
|
|
361
|
+
await ... - Use await for async operations
|
|
362
|
+
.exit - Exit REPL
|
|
363
|
+
`);
|
|
364
|
+
callback(null);
|
|
365
|
+
} else {
|
|
366
|
+
// Default eval
|
|
367
|
+
const result = await eval(cmd);
|
|
368
|
+
callback(null, result);
|
|
369
|
+
}
|
|
370
|
+
} catch (error) {
|
|
371
|
+
callback(error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
server.setupHistory(path.join(os.homedir(), '.s3db', 'history'), () => {});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Stats command
|
|
380
|
+
program
|
|
381
|
+
.command('stats [resource]')
|
|
382
|
+
.description('Show statistics')
|
|
383
|
+
.option('-c, --connection <string>', 'Connection string')
|
|
384
|
+
.action(async (resourceName, options) => {
|
|
385
|
+
const spinner = ora('Gathering stats...').start();
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const db = await getDatabase(options);
|
|
389
|
+
await db.init();
|
|
390
|
+
|
|
391
|
+
if (resourceName) {
|
|
392
|
+
const resource = await db.resource(resourceName);
|
|
393
|
+
const count = await resource.count();
|
|
394
|
+
spinner.stop();
|
|
395
|
+
|
|
396
|
+
console.log(chalk.cyan(`\nResource: ${resourceName}`));
|
|
397
|
+
console.log(`Total records: ${count}`);
|
|
398
|
+
} else {
|
|
399
|
+
const resources = await db.listResources();
|
|
400
|
+
spinner.stop();
|
|
401
|
+
|
|
402
|
+
console.log(chalk.cyan('\nDatabase Statistics'));
|
|
403
|
+
console.log(`Total resources: ${resources.length}`);
|
|
404
|
+
|
|
405
|
+
if (resources.length > 0) {
|
|
406
|
+
const table = new Table({
|
|
407
|
+
head: ['Resource', 'Count'],
|
|
408
|
+
style: { head: ['cyan'] }
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
for (const r of resources) {
|
|
412
|
+
const resource = await db.resource(r.name);
|
|
413
|
+
const count = await resource.count();
|
|
414
|
+
table.push([r.name, count]);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
console.log(table.toString());
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
spinner.fail(chalk.red(error.message));
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
program.parse(process.argv);
|
package/src/client.class.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import tryFn from "./concerns/try-fn.js";
|
|
21
21
|
import { md5 } from "./concerns/crypto.js";
|
|
22
22
|
import { idGenerator } from "./concerns/id.js";
|
|
23
|
+
import { metadataEncode, metadataDecode } from "./concerns/metadata-encoding.js";
|
|
23
24
|
import { ConnectionString } from "./connection-string.class.js";
|
|
24
25
|
import { mapAwsError, UnknownError, NoSuchKey, NotFound } from "./errors.js";
|
|
25
26
|
|
|
@@ -119,23 +120,16 @@ export class Client extends EventEmitter {
|
|
|
119
120
|
const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
|
|
120
121
|
const fullKey = keyPrefix ? path.join(keyPrefix, key) : key;
|
|
121
122
|
|
|
122
|
-
// Ensure all metadata values are strings and
|
|
123
|
+
// Ensure all metadata values are strings and use smart encoding
|
|
123
124
|
const stringMetadata = {};
|
|
124
125
|
if (metadata) {
|
|
125
126
|
for (const [k, v] of Object.entries(metadata)) {
|
|
126
|
-
// Ensure key is a valid string
|
|
127
|
+
// Ensure key is a valid string
|
|
127
128
|
const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, '_');
|
|
128
|
-
const stringValue = String(v);
|
|
129
129
|
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
if (hasSpecialChars) {
|
|
134
|
-
// Encode as base64 without prefix - we'll detect it intelligently on read
|
|
135
|
-
stringMetadata[validKey] = Buffer.from(stringValue, 'utf8').toString('base64');
|
|
136
|
-
} else {
|
|
137
|
-
stringMetadata[validKey] = stringValue;
|
|
138
|
-
}
|
|
130
|
+
// Smart encode the value
|
|
131
|
+
const { encoded } = metadataEncode(v);
|
|
132
|
+
stringMetadata[validKey] = encoded;
|
|
139
133
|
}
|
|
140
134
|
}
|
|
141
135
|
|
|
@@ -178,30 +172,11 @@ export class Client extends EventEmitter {
|
|
|
178
172
|
try {
|
|
179
173
|
response = await this.sendCommand(new GetObjectCommand(options));
|
|
180
174
|
|
|
181
|
-
// Smart decode
|
|
175
|
+
// Smart decode metadata values
|
|
182
176
|
if (response.Metadata) {
|
|
183
177
|
const decodedMetadata = {};
|
|
184
178
|
for (const [key, value] of Object.entries(response.Metadata)) {
|
|
185
|
-
|
|
186
|
-
// Try to decode as base64 and check if it's valid UTF-8 with special chars
|
|
187
|
-
try {
|
|
188
|
-
const decoded = Buffer.from(value, 'base64').toString('utf8');
|
|
189
|
-
// Check if decoded string is different from original and contains non-ASCII chars
|
|
190
|
-
const hasSpecialChars = /[^\x00-\x7F]/.test(decoded);
|
|
191
|
-
const isValidBase64 = Buffer.from(decoded, 'utf8').toString('base64') === value;
|
|
192
|
-
|
|
193
|
-
if (isValidBase64 && hasSpecialChars && decoded !== value) {
|
|
194
|
-
decodedMetadata[key] = decoded;
|
|
195
|
-
} else {
|
|
196
|
-
decodedMetadata[key] = value;
|
|
197
|
-
}
|
|
198
|
-
} catch (decodeError) {
|
|
199
|
-
// If decode fails, use original value
|
|
200
|
-
decodedMetadata[key] = value;
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
decodedMetadata[key] = value;
|
|
204
|
-
}
|
|
179
|
+
decodedMetadata[key] = metadataDecode(value);
|
|
205
180
|
}
|
|
206
181
|
response.Metadata = decodedMetadata;
|
|
207
182
|
}
|