kempo-inventory 0.2.2 → 0.3.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 +10 -12
- package/admin/nav.global.html +1 -1
- package/kempo-config.json +1 -7
- package/package.json +8 -3
- package/public/components/ItemFields.js +3 -3
- package/server/utils/media.js +25 -35
- package/update.js +4 -1
package/README.md
CHANGED
|
@@ -130,7 +130,7 @@ Reading the inventory needs the `items:read` permission. Turn on the `public_rea
|
|
|
130
130
|
|
|
131
131
|
## Who can see photos
|
|
132
132
|
|
|
133
|
-
kempo-files only serves a file to people with its `files:download` permission, which someone who can merely *read* the inventory does not have, so their photos would be broken images. The `public_photos` setting (on by default) uploads photos to kempo-
|
|
133
|
+
kempo-files only serves a file to people with its `files:download` permission, which someone who can merely *read* the inventory does not have, so their photos would be broken images. The `public_photos` setting (on by default) uploads photos as **public**, whether they go to kempo-media or kempo-files: viewable by anyone who has the link (ids are unguessable), which includes every inventory reader. Turn it off to keep photos behind `files:download`; then give your readers that permission. Changing the setting only affects new uploads. `update.js` also marks the photos items and categories already use (and their thumbnails) public when the setting is on.
|
|
134
134
|
|
|
135
135
|
## Photos and files (optional)
|
|
136
136
|
|
|
@@ -138,21 +138,15 @@ Three optional extensions can take part. Inventory never imports any of them up
|
|
|
138
138
|
|
|
139
139
|
| Extension | Role |
|
|
140
140
|
|---|---|
|
|
141
|
-
| [kempo-media](https://github.com/dustinpoissant/kempo-media) | A media library
|
|
142
|
-
| [kempo-files](https://github.com/dustinpoissant/kempo-files) | A
|
|
143
|
-
| [kempo-thumbs](https://github.com/dustinpoissant/kempo-thumbs) | Generates thumbnails for kempo-files, including frames from video and audio
|
|
141
|
+
| [kempo-media](https://github.com/dustinpoissant/kempo-media) | A media library: a kempo-files file plus kind, dimensions and alt text, with thumbnails from kempo-thumbs. It requires the other two, so installing it brings all three |
|
|
142
|
+
| [kempo-files](https://github.com/dustinpoissant/kempo-files) | A file library: files live outside `public/` and every download is permission-checked, unless the file is marked public |
|
|
143
|
+
| [kempo-thumbs](https://github.com/dustinpoissant/kempo-thumbs) | Generates thumbnails for kempo-files, including frames from video and audio |
|
|
144
144
|
|
|
145
145
|
When an item has more than one photo, the first is its **primary** photo (the one shown on cards and lists). In the item form each other photo has a **Make primary** button that moves it to the front.
|
|
146
146
|
|
|
147
|
-
Add a field of type "Photos / files" and every item gets an upload area. New uploads go to one
|
|
147
|
+
Add a field of type "Photos / files" and every item gets an upload area. New uploads go to kempo-media when it is installed (it is kempo-files with thumbnails, so one upload gives you a stored file and its thumbnail), and to kempo-files directly on a site that has only that. There is no setting to choose; the old `media_provider` setting was retired, and `update.js` removes it.
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|---|---|
|
|
151
|
-
| `auto` (default) | kempo-files when kempo-thumbs is also installed, otherwise kempo-media, otherwise kempo-files |
|
|
152
|
-
| `kempo-media` | kempo-media (if installed) |
|
|
153
|
-
| `kempo-files` | kempo-files (if installed) |
|
|
154
|
-
|
|
155
|
-
A stored value records where its file lives, so files from both libraries keep working on the same item whichever one takes new uploads. The API returns a `media` map (stored id → `{ kind, name, alt, path, thumbnail }`) next to `linked`, so a client can show them. A kempo-files photo has no `thumbnail` until kempo-thumbs has generated one, so clients should fall back to `path`.
|
|
149
|
+
A stored value records where its file lives (a bare id is a kempo-media asset, `files:<id>` a kempo-files file), so values written before this change, or by a site with only kempo-files, keep working on the same item as newer ones. The API returns a `media` map (stored id → `{ kind, name, alt, path, thumbnail }`) next to `linked`, so a client can show them. A kempo-files photo has no `thumbnail` until kempo-thumbs has generated one, so clients should fall back to `path`.
|
|
156
150
|
|
|
157
151
|
- If a library is disabled later, items keep their stored ids and everything else keeps working. Files from that library show as missing, and only *adding* a file from it is refused; an item can still be edited with its existing list unchanged.
|
|
158
152
|
- Deleting a file from its library leaves the id on the item, shown as a missing file.
|
|
@@ -448,3 +442,7 @@ All routes require a session holding the permission above.
|
|
|
448
442
|
- Uninstalling drops the item, field and movement tables, including your data.
|
|
449
443
|
- Custom values are stored in a single `jsonb` column, so adding a field never alters the schema.
|
|
450
444
|
- `item` links store the linked item's id. Deleting an item leaves any links to it in place, shown as "(deleted item)".
|
|
445
|
+
|
|
446
|
+
## Development
|
|
447
|
+
|
|
448
|
+
The documentation site is written in `docs-src/` and built into `docs/` (served by GitHub Pages from the `docs` folder). Edit `docs-src/`, never `docs/`, then run `npm run docs:build`; `npm run docs:dev` serves it on port 4050. Screenshots come in pairs: write `<img src="./media/name.png" ...>` in a page, and put `name-light.png` and `name-dark.png` in `docs-src/media`. The build turns the tag into both images and the site shows the one matching the visitor's theme.
|
package/admin/nav.global.html
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<content id="kempo-inventory-nav" name="Inventory" owner="kempo-inventory" enabled="true" locked="true" location="admin-nav-extensions" priority="20">
|
|
2
|
-
<k-aside-item icon="archive" href="/admin/extension/kempo-inventory/" no-expand>Inventory</k-aside-item>
|
|
2
|
+
<k-aside-item icon="archive" group="Commerce" href="/admin/extension/kempo-inventory/" no-expand>Inventory</k-aside-item>
|
|
3
3
|
</content>
|
package/kempo-config.json
CHANGED
|
@@ -63,12 +63,6 @@
|
|
|
63
63
|
}
|
|
64
64
|
],
|
|
65
65
|
"settings": [
|
|
66
|
-
{
|
|
67
|
-
"name": "media_provider",
|
|
68
|
-
"value": "auto",
|
|
69
|
-
"type": "string",
|
|
70
|
-
"description": "Where new photos and files are uploaded: auto, kempo-media or kempo-files. Auto uses kempo-files when kempo-thumbs is installed to thumbnail it, otherwise kempo-media."
|
|
71
|
-
},
|
|
72
66
|
{
|
|
73
67
|
"name": "import_max_mb",
|
|
74
68
|
"value": 250,
|
|
@@ -85,7 +79,7 @@
|
|
|
85
79
|
"name": "public_photos",
|
|
86
80
|
"value": true,
|
|
87
81
|
"type": "boolean",
|
|
88
|
-
"description": "
|
|
82
|
+
"description": "New photos are viewable by anyone with the link, so every person who can read the inventory sees them (a private file otherwise needs each of them to have the files:download permission). Turn off to keep photos behind that permission."
|
|
89
83
|
},
|
|
90
84
|
{
|
|
91
85
|
"name": "category_image_ratio",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-inventory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A basic inventory management system for kempo cms",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Dustin Poissant",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"main": "sdk.js",
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "node --test",
|
|
19
|
-
"link:local": "npm install ../kempo --no-save"
|
|
19
|
+
"link:local": "npm install ../kempo ../kempo-server ../kempo-ui --no-save --no-legacy-peer-deps --force",
|
|
20
|
+
"docs:build": "node scripts/build-docs.js",
|
|
21
|
+
"docs:dev": "npx kempo-server --root docs --port 4050"
|
|
20
22
|
},
|
|
21
23
|
"kempo": {
|
|
22
24
|
"public-scope": "inventory"
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
"peerDependencies": {
|
|
42
44
|
"kempo": ">=4.3.0",
|
|
43
45
|
"kempo-ui": ">=0.5.0",
|
|
44
|
-
"kempo-media": ">=0.
|
|
46
|
+
"kempo-media": ">=0.3.0",
|
|
45
47
|
"kempo-files": ">=0.1.3",
|
|
46
48
|
"kempo-thumbs": ">=0.1.3"
|
|
47
49
|
},
|
|
@@ -55,5 +57,8 @@
|
|
|
55
57
|
"kempo-thumbs": {
|
|
56
58
|
"optional": true
|
|
57
59
|
}
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"highlight.js": "^11.11.1"
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -42,7 +42,7 @@ const MEDIA_ACCEPT = 'image/*,video/*,audio/*,.glb,.gltf,.obj,.fbx,.zip';
|
|
|
42
42
|
|
|
43
43
|
/*
|
|
44
44
|
Which library new uploads go to ('media', 'files' or null), decided by the server from what is
|
|
45
|
-
installed
|
|
45
|
+
installed: kempo-media when it is there, otherwise kempo-files. Asked once per page.
|
|
46
46
|
*/
|
|
47
47
|
let infoPromise = null;
|
|
48
48
|
const fieldInfo = () => infoPromise ??= getFields().then(([error, data]) => {
|
|
@@ -50,7 +50,7 @@ const fieldInfo = () => infoPromise ??= getFields().then(([error, data]) => {
|
|
|
50
50
|
return error ? null : data;
|
|
51
51
|
});
|
|
52
52
|
const uploadProvider = () => fieldInfo().then(data => data?.capabilities.upload ?? null);
|
|
53
|
-
/* Whether
|
|
53
|
+
/* Whether new uploads are made public (the public_photos setting). */
|
|
54
54
|
const uploadsArePublic = () => fieldInfo().then(data => data?.capabilities.publicFiles !== false);
|
|
55
55
|
|
|
56
56
|
/*
|
|
@@ -66,7 +66,7 @@ export const imageRatios = () => fieldInfo().then(data => data?.imageRatios ?? {
|
|
|
66
66
|
*/
|
|
67
67
|
const uploadToMedia = async file => {
|
|
68
68
|
const { uploadMedia } = await import('/kempo-media/sdk.js');
|
|
69
|
-
const [error, data] = await uploadMedia(file);
|
|
69
|
+
const [error, data] = await uploadMedia(file, { public: await uploadsArePublic() });
|
|
70
70
|
if(error) return [error, null];
|
|
71
71
|
const asset = data.asset;
|
|
72
72
|
return [null, { id: asset.id, asset: {
|
package/server/utils/media.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
|
-
import { join } from 'path';
|
|
3
2
|
import { getExtension, getSetting, triggerHook } from 'kempo/server/sdk.js';
|
|
4
|
-
import {
|
|
3
|
+
import { safeFileName } from './mediaSafety.js';
|
|
5
4
|
import { DEFAULT_RATIOS, resolveRatio } from './ratio.js';
|
|
6
5
|
import { parseMediaId, filesId } from './fieldTypes.js';
|
|
7
6
|
|
|
@@ -9,16 +8,18 @@ import { parseMediaId, filesId } from './fieldTypes.js';
|
|
|
9
8
|
Where item photos and files live. Three optional extensions can take part, and inventory works
|
|
10
9
|
the same with none of them installed:
|
|
11
10
|
|
|
12
|
-
kempo-media a media library
|
|
13
|
-
|
|
11
|
+
kempo-media a media library: a kempo-files file plus kind, dimensions and alt text, with
|
|
12
|
+
thumbnails from kempo-thumbs. It requires the other two.
|
|
13
|
+
kempo-files a file library; a non-public file needs files:download to view
|
|
14
14
|
kempo-thumbs generates thumbnails for kempo-files (images, and frames from video and audio)
|
|
15
15
|
|
|
16
16
|
Nothing here imports them up front. Each is loaded on demand and only counts as available when it
|
|
17
|
-
is both installed as a package and enabled in kempo.
|
|
18
|
-
kempo-files, it is only ever used for files stored there.
|
|
17
|
+
is both installed as a package and enabled in kempo.
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
New uploads go to kempo-media when it is there, because it already is kempo-files with thumbnails;
|
|
20
|
+
only a site with kempo-files alone uploads to kempo-files directly. Stored values keep saying where
|
|
21
|
+
they live, so older ones keep resolving: a bare id is a kempo-media asset, "files:<id>" is a
|
|
22
|
+
kempo-files file.
|
|
22
23
|
*/
|
|
23
24
|
const loaders = {
|
|
24
25
|
'kempo-media': () => import('kempo-media/sdk'),
|
|
@@ -51,22 +52,11 @@ export const mediaAvailable = async () => {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
/*
|
|
54
|
-
Which
|
|
55
|
-
|
|
56
|
-
The `media_provider` setting can force one ('kempo-media' or 'kempo-files'). Left on 'auto',
|
|
57
|
-
kempo-files wins when kempo-thumbs is there to thumbnail it, because that is a better result than
|
|
58
|
-
kempo-media's; otherwise kempo-media wins, since a bare kempo-files has no thumbnails at all.
|
|
55
|
+
Which library new uploads go to: 'media', 'files' or null when neither is installed.
|
|
59
56
|
*/
|
|
60
|
-
export const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if(choice === 'kempo-media' && found.media) return 'media';
|
|
64
|
-
if(choice === 'kempo-files' && found.files) return 'files';
|
|
65
|
-
if(found.files && found.thumbs) return 'files';
|
|
66
|
-
if(found.media) return 'media';
|
|
67
|
-
if(found.files) return 'files';
|
|
68
|
-
return null;
|
|
69
|
-
};
|
|
57
|
+
export const chooseUploadProvider = found => found.media ? 'media' : found.files ? 'files' : null;
|
|
58
|
+
|
|
59
|
+
export const uploadProvider = async () => chooseUploadProvider(await providers());
|
|
70
60
|
|
|
71
61
|
/*
|
|
72
62
|
The aspect ratios images are shown at, as CSS ("4 / 3"): { category, item }. They come from the
|
|
@@ -78,7 +68,7 @@ export const getImageRatios = async () => {
|
|
|
78
68
|
return { category: resolveRatio(category, 'category'), item: resolveRatio(item, 'item') };
|
|
79
69
|
};
|
|
80
70
|
|
|
81
|
-
/* Whether photos
|
|
71
|
+
/* Whether new photos are made public, so people who can only read the inventory can see them. */
|
|
82
72
|
export const publicPhotos = async () => {
|
|
83
73
|
const [, value] = await getSetting('kempo-inventory', 'public_photos', true);
|
|
84
74
|
return value !== false && value !== 'false';
|
|
@@ -173,7 +163,13 @@ export const readMedia = async storedId => {
|
|
|
173
163
|
const { getMediaAsset } = await load('kempo-media');
|
|
174
164
|
const [error, asset] = await getMediaAsset(rawId);
|
|
175
165
|
if(error) return [error, null];
|
|
176
|
-
|
|
166
|
+
// The asset's id is its kempo-files file id, and its bytes live in kempo-files
|
|
167
|
+
const { getFile, filePath } = await load('kempo-files');
|
|
168
|
+
const [fileError, file] = await getFile(asset.fileId);
|
|
169
|
+
if(fileError) return [fileError, null];
|
|
170
|
+
const [pathError, absolute] = await filePath(file);
|
|
171
|
+
if(pathError) return [pathError, null];
|
|
172
|
+
return [null, { name: asset.originalName, kind: asset.kind, alt: asset.altText ?? '', bytes: await readFile(absolute) }];
|
|
177
173
|
}
|
|
178
174
|
} catch {
|
|
179
175
|
return [{ code: 404, msg: 'The file could not be read' }, null];
|
|
@@ -181,8 +177,6 @@ export const readMedia = async storedId => {
|
|
|
181
177
|
return [{ code: 409, msg: 'The library holding this file is not installed' }, null];
|
|
182
178
|
};
|
|
183
179
|
|
|
184
|
-
const MIME = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', avif: 'image/avif', bmp: 'image/bmp', mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4', pdf: 'application/pdf' };
|
|
185
|
-
|
|
186
180
|
/*
|
|
187
181
|
Stores a file in whichever library new uploads go to (the same choice as an upload from the form), on
|
|
188
182
|
behalf of `userId`, and resolves to [null, storedId]. The caller is responsible for having checked who
|
|
@@ -212,16 +206,12 @@ export const storeMedia = async ({ name, bytes, altText = '', userId }) => {
|
|
|
212
206
|
}
|
|
213
207
|
return [{ code: 409, msg: 'No free file name could be found' }, null];
|
|
214
208
|
}
|
|
215
|
-
const {
|
|
209
|
+
const { uploadMediaAsset } = await load('kempo-media');
|
|
216
210
|
const [, maxMb] = await getSetting('kempo-media', 'max_upload_size_mb', 250);
|
|
217
|
-
const [,
|
|
218
|
-
|
|
219
|
-
rootDir: join(process.cwd(), 'public'), filename: safe, data, mimeType: MIME[extensionOf(safe)] ?? 'application/octet-stream',
|
|
220
|
-
altText, uploadedBy: userId, maxBytes: Number(maxMb) * 1024 * 1024, thumbnailMaxDimension: Number(thumbnailMaxDimension),
|
|
211
|
+
const [error, asset] = await uploadMediaAsset({
|
|
212
|
+
filename: safe, data, altText, uploadedBy: userId, maxBytes: Number(maxMb) * 1024 * 1024, public: await publicPhotos(),
|
|
221
213
|
});
|
|
222
|
-
|
|
223
|
-
const [recordError, row] = await createMediaAsset(asset);
|
|
224
|
-
return recordError ? [recordError, null] : [null, row.id];
|
|
214
|
+
return error ? [error, null] : [null, asset.id];
|
|
225
215
|
} catch {
|
|
226
216
|
return [{ code: 500, msg: 'The file could not be stored' }, null];
|
|
227
217
|
}
|
package/update.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
2
|
import db from 'kempo/server/db/index.js';
|
|
3
|
-
import { getSetting, setSetting } from 'kempo/server/sdk.js';
|
|
3
|
+
import { getSetting, setSetting, deleteSetting } from 'kempo/server/sdk.js';
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
kempo creates tables that are new in an update but never alters ones that already exist, so a
|
|
@@ -8,6 +8,9 @@ import { getSetting, setSetting } from 'kempo/server/sdk.js';
|
|
|
8
8
|
this on a database that already has the change is harmless.
|
|
9
9
|
*/
|
|
10
10
|
export default async () => {
|
|
11
|
+
// Next: media_provider is retired. kempo-media sits on kempo-files, so new uploads go through it whenever it is installed.
|
|
12
|
+
await deleteSetting('kempo-inventory', 'media_provider');
|
|
13
|
+
|
|
11
14
|
// 0.15: items and categories can be owned by an extension ('' = managed by people, as before)
|
|
12
15
|
await db.execute(sql`ALTER TABLE "kempoInventoryItem" ADD COLUMN IF NOT EXISTS "owner" text NOT NULL DEFAULT ''`);
|
|
13
16
|
await db.execute(sql`ALTER TABLE "kempoInventoryCategory" ADD COLUMN IF NOT EXISTS "owner" text NOT NULL DEFAULT ''`);
|