@3sln/trove 0.0.5 → 0.0.7
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
CHANGED
|
@@ -163,6 +163,12 @@ TROVE_S3_ACCESS_KEY_ID=… # or AWS_ACCESS_KEY_ID
|
|
|
163
163
|
TROVE_S3_SECRET_ACCESS_KEY=… # or AWS_SECRET_ACCESS_KEY
|
|
164
164
|
TROVE_S3_PATH_STYLE=true # MinIO / custom endpoints
|
|
165
165
|
|
|
166
|
+
# Which store types a COLLECTION may be created on. Defaults to everything this
|
|
167
|
+
# runtime registered; naming a subset takes the rest off the collection form and
|
|
168
|
+
# refuses them. Worth setting on Workers, where `memory` is offered because it is
|
|
169
|
+
# portable but produces a collection that loses its uploads on isolate recycle.
|
|
170
|
+
TROVE_STORAGE_DRIVERS=s3 # subset of: memory | filesystem | s3
|
|
171
|
+
|
|
166
172
|
# Metadata (file tree + facets)
|
|
167
173
|
TROVE_METADATA=sqlite # memory | sqlite
|
|
168
174
|
TROVE_DB_PATH=./data/trove.db
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@3sln/trove",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Trove — a self-hostable, plugin-extensible Google Drive. Semantic search, pluggable storage (S3 / filesystem / NAS), and a VS Code-style contribution system with sandboxed plugins.",
|
|
6
6
|
"repository": {
|
|
@@ -49,9 +49,19 @@ export function portableDrivers() {
|
|
|
49
49
|
help: 'MinIO and most self-hosted endpoints need this.',
|
|
50
50
|
},
|
|
51
51
|
],
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
52
|
+
// Two config shapes reach here and both have to work.
|
|
53
|
+
//
|
|
54
|
+
// Flat — `{ driver: 's3', bucket: … }` — is what `fields` above describes and what
|
|
55
|
+
// the collection form posts. Nested under `s3` is what `configFromEnv` produces and
|
|
56
|
+
// what every collection record written before this driver existed holds, because the
|
|
57
|
+
// switch this replaced read `cfg.s3` and nothing else.
|
|
58
|
+
//
|
|
59
|
+
// Normalising rather than only handling it in `create` is the point: validation runs
|
|
60
|
+
// against the normalised shape, so a nested config is no longer refused for a missing
|
|
61
|
+
// top-level `bucket` that was about to be spread in anyway. That refusal broke every
|
|
62
|
+
// environment-configured S3 deployment at startup, which is as loud as a bug gets and
|
|
63
|
+
// still took a local run to see, because no test used the env shape.
|
|
64
|
+
normalize: (cfg) => ({ ...cfg, ...(cfg.s3 || {}) }),
|
|
55
65
|
create: (cfg) => new S3Storage({
|
|
56
66
|
bucket: cfg.bucket,
|
|
57
67
|
region: cfg.region || 'auto',
|
|
@@ -60,7 +70,6 @@ export function portableDrivers() {
|
|
|
60
70
|
secretAccessKey: cfg.secretAccessKey,
|
|
61
71
|
sessionToken: cfg.sessionToken,
|
|
62
72
|
forcePathStyle: cfg.forcePathStyle === true || cfg.forcePathStyle === 'true',
|
|
63
|
-
...(cfg.s3 || {}),
|
|
64
73
|
}),
|
|
65
74
|
},
|
|
66
75
|
{
|
|
@@ -56,6 +56,9 @@ export class StorageDriverRegistry {
|
|
|
56
56
|
* @param {string} driver.label for a form
|
|
57
57
|
* @param {string} [driver.description] one line about what it is
|
|
58
58
|
* @param {DriverField[]} [driver.fields]
|
|
59
|
+
* @param {(config: object) => object} [driver.normalize] accept an older or alternative
|
|
60
|
+
* config shape, returning the one `fields` describes. Runs before validation, so the
|
|
61
|
+
* shape checked is the shape built from.
|
|
59
62
|
* @param {(config: object) => StorageBackend} driver.create
|
|
60
63
|
*/
|
|
61
64
|
register(driver) {
|
|
@@ -82,6 +85,7 @@ export class StorageDriverRegistry {
|
|
|
82
85
|
placeholder: f.placeholder || '',
|
|
83
86
|
help: f.help || '',
|
|
84
87
|
})),
|
|
88
|
+
normalize: driver.normalize || null,
|
|
85
89
|
create: driver.create,
|
|
86
90
|
});
|
|
87
91
|
return this;
|
|
@@ -95,9 +99,30 @@ export class StorageDriverRegistry {
|
|
|
95
99
|
return [...this._drivers.keys()];
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* One registered driver, `create` included — unlike `describe()`, which deliberately
|
|
104
|
+
* strips it because it answers a client.
|
|
105
|
+
*
|
|
106
|
+
* For copying a driver into another registry, which is how a deployment narrows the set
|
|
107
|
+
* it offers: a registry has no `unregister`, so narrowing rebuilds from what survived
|
|
108
|
+
* rather than removing from what did not. Keeping removal out of this class is the point
|
|
109
|
+
* — a driver disappearing from a live registry is a store that stops being buildable
|
|
110
|
+
* while collections still reference it.
|
|
111
|
+
*/
|
|
112
|
+
driver(key) {
|
|
113
|
+
return this._drivers.get(key);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* What a client needs to render a form.
|
|
118
|
+
*
|
|
119
|
+
* Neither `create` nor `normalize`: both are behaviour, not data. JSON.stringify would
|
|
120
|
+
* drop them anyway, which is exactly why they are removed here instead — a describe()
|
|
121
|
+
* whose result is only serialisable by accident is one that leaks the next function
|
|
122
|
+
* somebody adds into every in-process consumer.
|
|
123
|
+
*/
|
|
99
124
|
describe() {
|
|
100
|
-
return [...this._drivers.values()].map(({ create, ...rest }) => rest);
|
|
125
|
+
return [...this._drivers.values()].map(({ create, normalize, ...rest }) => rest);
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
/**
|
|
@@ -105,6 +130,13 @@ export class StorageDriverRegistry {
|
|
|
105
130
|
*
|
|
106
131
|
* An unknown driver throws, and says what IS available. This is the arm that used to
|
|
107
132
|
* return an in-memory store.
|
|
133
|
+
*
|
|
134
|
+
* `normalize` runs FIRST, so a driver that accepts more than one config shape validates
|
|
135
|
+
* the shape it will actually build from. Without it, required-field checks are performed
|
|
136
|
+
* against a config the driver was about to rewrite — which is precisely how S3 broke:
|
|
137
|
+
* `configFromEnv` nests its settings under `s3`, `create` spread that back out, and the
|
|
138
|
+
* check in between looked for a top-level `bucket` that was never going to be there and
|
|
139
|
+
* refused every environment-configured S3 deployment at startup.
|
|
108
140
|
*/
|
|
109
141
|
build(config) {
|
|
110
142
|
const key = config?.driver;
|
|
@@ -115,12 +147,13 @@ export class StorageDriverRegistry {
|
|
|
115
147
|
`Unknown storage driver "${key}" — this deployment has: ${this.keys().join(', ') || 'none'}`,
|
|
116
148
|
);
|
|
117
149
|
}
|
|
150
|
+
const cfg = driver.normalize ? driver.normalize(config) : config;
|
|
118
151
|
for (const f of driver.fields) {
|
|
119
|
-
if (f.required && (
|
|
152
|
+
if (f.required && (cfg[f.name] == null || cfg[f.name] === '')) {
|
|
120
153
|
throw TroveError.invalid(`Storage driver "${key}" requires "${f.name}"`);
|
|
121
154
|
}
|
|
122
155
|
}
|
|
123
|
-
const backend = driver.create(
|
|
156
|
+
const backend = driver.create(cfg);
|
|
124
157
|
if (!(backend instanceof StorageBackend)) {
|
|
125
158
|
throw TroveError.invalid(`Storage driver "${key}" did not return a StorageBackend`);
|
|
126
159
|
}
|
|
@@ -58,13 +58,41 @@ const resolve = (value, BaseClass, build) =>
|
|
|
58
58
|
* portable ones — so a deployment adds Filesystem (Node/Bun), or a driver written
|
|
59
59
|
* entirely outside this package, by naming it at the entry point. What is not registered
|
|
60
60
|
* is not offered and cannot be built.
|
|
61
|
+
*
|
|
62
|
+
* `config.allowedStorageDrivers` (TROVE_STORAGE_DRIVERS) narrows the result to an explicit
|
|
63
|
+
* set. Adding drivers is a code decision made by an entry point, which knows what the
|
|
64
|
+
* runtime can run; REMOVING them is an operator decision about one deployment, and needs
|
|
65
|
+
* to be reachable from configuration alone. A drive on Workers is the case in point: memory
|
|
66
|
+
* is portable, so it is offered, and choosing it there produces a collection that accepts
|
|
67
|
+
* uploads and loses them when the isolate is recycled. `TROVE_STORAGE_DRIVERS=s3` takes it
|
|
68
|
+
* off the menu without a fork of the entry point.
|
|
69
|
+
*
|
|
70
|
+
* A name that matches nothing throws rather than narrowing to nothing — a typo that left a
|
|
71
|
+
* drive with no way to make a collection would be a puzzle, not a message.
|
|
61
72
|
*/
|
|
62
73
|
export function storageRegistry(config = {}) {
|
|
63
74
|
if (config.storageRegistry instanceof StorageDriverRegistry) return config.storageRegistry;
|
|
64
75
|
if (config.storageDrivers instanceof StorageDriverRegistry) return config.storageDrivers;
|
|
65
76
|
const registry = new StorageDriverRegistry(portableDrivers());
|
|
66
77
|
for (const d of config.storageDrivers || []) registry.register(d);
|
|
67
|
-
|
|
78
|
+
|
|
79
|
+
const allowed = config.allowedStorageDrivers;
|
|
80
|
+
if (!allowed?.length) return registry;
|
|
81
|
+
const unknown = allowed.filter((k) => !registry.has(k));
|
|
82
|
+
if (unknown.length) {
|
|
83
|
+
throw TroveError.invalid(
|
|
84
|
+
`TROVE_STORAGE_DRIVERS names ${unknown.map((k) => `"${k}"`).join(', ')}, which `
|
|
85
|
+
+ `${unknown.length === 1 ? 'is not a driver' : 'are not drivers'} this deployment has: `
|
|
86
|
+
+ `${registry.keys().join(', ')}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
// Rebuilt from the descriptors that survived rather than mutated, so a registry never
|
|
90
|
+
// has to support removal — and the drivers keep their registration order.
|
|
91
|
+
const narrowed = new StorageDriverRegistry();
|
|
92
|
+
for (const key of registry.keys()) {
|
|
93
|
+
if (allowed.includes(key)) narrowed.register(registry.driver(key));
|
|
94
|
+
}
|
|
95
|
+
return narrowed;
|
|
68
96
|
}
|
|
69
97
|
|
|
70
98
|
/**
|
|
@@ -624,6 +624,14 @@ export function configFromEnv(env = (typeof process !== 'undefined' ? process.en
|
|
|
624
624
|
if (config.storage.driver === 'filesystem') config.storage.root = env.TROVE_FS_ROOT || './data/objects';
|
|
625
625
|
if (config.storage.driver === 's3') config.storage.s3 = s3FromEnv(env, 'TROVE_');
|
|
626
626
|
|
|
627
|
+
// Which store types a COLLECTION may be created on, if not all of the ones this entry
|
|
628
|
+
// point registered. `TROVE_STORAGE` picks the drive's own primary store; this restricts
|
|
629
|
+
// the menu the collection form offers and what `build` will accept — see
|
|
630
|
+
// engine/providers/core.js for why removal is configuration and addition is code.
|
|
631
|
+
if (env.TROVE_STORAGE_DRIVERS) {
|
|
632
|
+
config.allowedStorageDrivers = env.TROVE_STORAGE_DRIVERS.split(',').map((s) => s.trim()).filter(Boolean);
|
|
633
|
+
}
|
|
634
|
+
|
|
627
635
|
config.metadata.driver = env.TROVE_METADATA || (config.storage.driver === 'memory' ? 'memory' : 'sqlite');
|
|
628
636
|
config.metadata.path = env.TROVE_DB_PATH || './data/trove.db';
|
|
629
637
|
|