kempo-inventory 0.2.1 → 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 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-files as **public**: 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. kempo-media files are ordinary static files and are always viewable.
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 with its own image thumbnails. Files are served from `public/media/`, so anyone who knows a URL can fetch them |
142
- | [kempo-files](https://github.com/dustinpoissant/kempo-files) | A private file library: files live outside `public/` and every download is permission-checked |
143
- | [kempo-thumbs](https://github.com/dustinpoissant/kempo-thumbs) | Generates thumbnails for kempo-files, including frames from video and audio. It only works on kempo-files |
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 library, chosen by the `media_provider` setting:
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
- | Setting | Uploads go to |
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.
@@ -314,7 +308,8 @@ Every function resolves to `[error, result]`, where `error` is `{ code, msg }` o
314
308
  | `createItem({ sku, name, description, quantity, fields, userId }, { owner })` | Create. `owner` makes it your extension's |
315
309
  | `updateItem(id, { sku, name, description, fields, userId }, { owner })` | Update; `fields` is merged, and an empty value clears a field. SKU, name and category of an owned item need its `owner` |
316
310
  | `deleteItem(id, { userId, owner })` | Delete an item and its history (owned items need their `owner`) |
317
- | `adjustStock(id, { delta, reason, note, userId })` | The only way quantity changes, for anyone. Atomic; refuses to go below zero |
311
+ | `adjustStock(id, { delta, reason, note, userId })` | The way quantity changes, for anyone. Atomic; refuses to go below zero |
312
+ | `adjustStockMany(changes, { reason, note, ref, userId, owner })` | Several stock changes together: every one happens or none does. `changes` is `[{ id, delta }]`. Use it when one event touches several items, such as the materials of a recipe |
318
313
  | `unregisterItems(owner, { release })` | Delete every item you own, or with `release` hand them back to people |
319
314
  | `getMovements({ itemId, limit, offset })` | Stock history |
320
315
  | `getFields({ category, owner })`, `getField(key, category)` | Read field definitions. `getFields()` is every field (each with its `category` key and `categoryName`); with `category` it is what an item in that category shows |
@@ -328,6 +323,26 @@ Every function resolves to `[error, result]`, where `error` is `{ code, msg }` o
328
323
 
329
324
  An item looks like `{ id, sku, name, description, category, tags, quantity, fields: { ...custom values }, owner, created, updated }`.
330
325
 
326
+ ### Several items at once
327
+
328
+ `adjustStock` changes one item in its own transaction, so a sale that uses resin, paint and filament could take the resin and then find the paint short. `adjustStockMany` applies the whole set together:
329
+
330
+ ```javascript
331
+ import { adjustStockMany } from 'kempo-inventory/sdk';
332
+
333
+ const [error, { items }] = await adjustStockMany(
334
+ [{ id: resin.id, delta: -175 }, { id: paint.id, delta: -20 }],
335
+ { reason: 'sale', ref: 'order-1042', owner: 'my-extension' },
336
+ );
337
+ // error.code 409 and error.msg "Insufficient stock of Paint (ml)": nothing changed
338
+ ```
339
+
340
+ Changes to the same item are added together and one that nets to nothing is left out. Every item goes through the same `before_adjust` guard as `adjustStock`, and each fires `stock:adjusted` once everything is committed. `ref` is written on every stock movement (as `[order-1042]` in its note) so the change can be found, and reversed, later.
341
+
342
+ ### Adding to the item page
343
+
344
+ An extension can add to the admin item page by supplying a fragment named `inventory-item-actions`, in `admin/inventory-item-actions.fragment.html` in its own package. It appears under the item form, and reads the item from `?id=` in the address. (kempo-products-inventory uses it for "Make product from this item".)
345
+
331
346
  ### Hooks
332
347
 
333
348
  Declare these in your `kempo-config.json` `hooks`. Every payload also carries `actor`: the extension that made the change through the SDK, or `''` for a person using the admin.
@@ -427,3 +442,7 @@ All routes require a session holding the permission above.
427
442
  - Uninstalling drops the item, field and movement tables, including your data.
428
443
  - Custom values are stored in a single `jsonb` column, so adding a field never alters the schema.
429
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.
@@ -12,6 +12,12 @@ locked: true
12
12
 
13
13
  <div id="editItem" style="max-width: 44rem;"></div>
14
14
 
15
+ <!--
16
+ An extension can add to this page by supplying a fragment with this name in its own admin/
17
+ directory (inventory-item-actions.fragment.html). It reads the item from ?id= in the address.
18
+ -->
19
+ <fragment name="inventory-item-actions"></fragment>
20
+
15
21
  <script type="module">
16
22
  import { getItem, getFields } from '/inventory/sdk.js';
17
23
  import { getAllCurrentUserPermissions } from '/kempo/sdk.js';
@@ -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": "Photos uploaded to kempo-files are viewable by anyone with the link, so every person who can read the inventory sees them (kempo-files otherwise needs each of them to have its files:download permission). Turn off to keep photos behind that permission."
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.2.1",
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.2.3",
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 and the media_provider setting. Asked once per page.
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 files uploaded to kempo-files are made public (the public_photos setting). */
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/sdk.js CHANGED
@@ -32,6 +32,7 @@ export {
32
32
  deleteItem,
33
33
  unregisterItems,
34
34
  adjustStock,
35
+ adjustStockMany,
35
36
  getMovements,
36
37
  } from './server/utils/items.js';
37
38
 
@@ -377,6 +377,102 @@ export const adjustStock = async (id, { delta, reason = 'adjustment', note = '',
377
377
  }
378
378
  };
379
379
 
380
+ /*
381
+ Thrown inside a transaction to roll everything back with a message the caller can show.
382
+ */
383
+ class Refusal {
384
+ constructor(error){
385
+ this.error = error;
386
+ }
387
+ }
388
+
389
+ /*
390
+ Applies several stock changes together: every one happens or none does. This is what makes
391
+ taking the materials for a whole recipe safe, where one adjustStock per item could take the resin
392
+ and then fail on the paint. `changes` is [{ id, delta, reason?, note? }]; changes to the same item
393
+ are added together, and one that nets to nothing is left out. Each item runs through the same
394
+ before_adjust guard as adjustStock, and fires the same stock:adjusted notification once everything
395
+ is committed.
396
+
397
+ const [error, result] = await adjustStockMany(
398
+ [{ id: resin.id, delta: -175 }, { id: paint.id, delta: -20 }],
399
+ { reason: 'sale', ref: 'order-1042', owner: 'my-extension' },
400
+ );
401
+
402
+ `ref` is recorded on every movement so the change can be found and reversed later. A refusal for
403
+ lack of stock is a 409 that names the item, and nothing has changed. Resolves to
404
+ [null, { items }] with the items as they are now.
405
+ */
406
+ export const adjustStockMany = async (changes, { reason = 'adjustment', note = '', ref = '', userId = '', owner = '' } = {}) => {
407
+ if(!Array.isArray(changes) || !changes.length) return [{ code: 400, msg: 'changes must be a non-empty list' }, null];
408
+
409
+ const totals = new Map();
410
+ for(const change of changes){
411
+ const id = String(change?.id ?? '');
412
+ const delta = toInt(change?.delta, NaN);
413
+ if(!id) return [{ code: 400, msg: 'Every change needs an item id' }, null];
414
+ if(Number.isNaN(delta)) return [{ code: 400, msg: 'Every delta must be a whole number' }, null];
415
+ const entry = totals.get(id) ?? { id, delta: 0, reason: change.reason, note: change.note };
416
+ entry.delta += delta;
417
+ totals.set(id, entry);
418
+ }
419
+ /* In a fixed order, so two overlapping calls lock rows the same way round and cannot deadlock. */
420
+ const wanted = [...totals.values()].filter(entry => entry.delta !== 0).sort((a, b) => a.id.localeCompare(b.id));
421
+ if(!wanted.length) return [null, { items: [] }];
422
+
423
+ let rows;
424
+ try {
425
+ rows = await db.select().from(kempoInventoryItem).where(inArray(kempoInventoryItem.id, wanted.map(entry => entry.id)));
426
+ } catch {
427
+ return [{ code: 500, msg: 'Failed to adjust stock' }, null];
428
+ }
429
+ const existing = new Map(rows.map(row => [row.id, shape(row)]));
430
+ const missing = wanted.find(entry => !existing.has(entry.id));
431
+ if(missing) return [{ code: 404, msg: 'An item in this change no longer exists' }, null];
432
+
433
+ const drafts = new Map();
434
+ for(const entry of wanted){
435
+ const draft = { delta: entry.delta, reason: entry.reason ?? reason, note: entry.note ?? note };
436
+ const refused = await guard(EVENTS.stockBeforeAdjust, { item: existing.get(entry.id), draft, userId, actor: owner });
437
+ if(refused) return [refused, null];
438
+ const delta = toInt(draft.delta, NaN);
439
+ if(Number.isNaN(delta) || delta === 0) return [{ code: 400, msg: 'delta must be a non-zero whole number' }, null];
440
+ drafts.set(entry.id, { delta, reason: String(draft.reason || 'adjustment'), note: String(draft.note || '') });
441
+ }
442
+
443
+ try {
444
+ const applied = await db.transaction(async tx => {
445
+ const results = [];
446
+ for(const entry of wanted){
447
+ const { delta, reason: why, note: text } = drafts.get(entry.id);
448
+ const [row] = await tx.update(kempoInventoryItem)
449
+ .set({ quantity: sql`${kempoInventoryItem.quantity} + ${delta}`, updated: new Date() })
450
+ .where(and(eq(kempoInventoryItem.id, entry.id), sql`${kempoInventoryItem.quantity} + ${delta} >= 0`))
451
+ .returning();
452
+ if(!row) throw new Refusal({ code: 409, msg: `Insufficient stock of ${existing.get(entry.id).name}` });
453
+ const [movement] = await tx.insert(kempoInventoryMovement).values({
454
+ id: newId(), itemId: entry.id, delta, quantityAfter: row.quantity, reason: why,
455
+ note: ref ? `${text ? `${text} ` : ''}[${ref}]` : text, userId, created: new Date(),
456
+ }).returning();
457
+ results.push({ row, movement, delta });
458
+ }
459
+ return results;
460
+ });
461
+ const items = [];
462
+ for(const { row, movement, delta } of applied){
463
+ const item = shape(row);
464
+ items.push(item);
465
+ await notify(EVENTS.stockAdjusted, {
466
+ item, previousQuantity: item.quantity - delta, delta, reason: movement.reason, note: movement.note, movement, userId, actor: owner,
467
+ });
468
+ }
469
+ return [null, { items }];
470
+ } catch(error) {
471
+ if(error instanceof Refusal) return [error.error, null];
472
+ return [{ code: 500, msg: 'Failed to adjust stock' }, null];
473
+ }
474
+ };
475
+
380
476
  export const getMovements = async ({ itemId, limit = 50, offset = 0 } = {}) => {
381
477
  try {
382
478
  const where = itemId ? eq(kempoInventoryMovement.itemId, itemId) : undefined;
@@ -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 { extensionOf, safeFileName } from './mediaSafety.js';
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 with its own image thumbnails
13
- kempo-files a private file library; uploads need a signed-in user with files:download to view
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. Because kempo-thumbs only works on
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
- A stored value says where it lives: a bare id is a kempo-media asset, "files:<id>" is a
21
- kempo-files file. So values from either place keep resolving, whichever is used for new uploads.
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 provider new uploads go to: 'media', 'files' or null when neither is installed.
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 uploadProvider = async () => {
61
- const found = await providers();
62
- const [, choice] = await getSetting('kempo-inventory', 'media_provider', 'auto');
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 uploaded to kempo-files are made public, so people who can only read the inventory can see them. */
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
- return [null, { name: asset.originalName, kind: asset.kind, alt: asset.altText ?? '', bytes: await readFile(join(process.cwd(), 'public', asset.path)) }];
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 { storeUpload, createMediaAsset } = await load('kempo-media');
209
+ const { uploadMediaAsset } = await load('kempo-media');
216
210
  const [, maxMb] = await getSetting('kempo-media', 'max_upload_size_mb', 250);
217
- const [, thumbnailMaxDimension] = await getSetting('kempo-media', 'thumbnail_max_dimension', 480);
218
- const [error, asset] = await storeUpload({
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
- if(error) return [error, null];
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 ''`);