kempo-inventory 0.5.7 → 0.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-inventory",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "A basic inventory management system for kempo cms",
5
5
  "license": "MIT",
6
6
  "author": "Dustin Poissant <dustinpoissant@gmail.com>",
@@ -22,7 +22,7 @@ import { getFields, createField, updateField, deleteField, getCategories } from
22
22
  */
23
23
  const TYPE_LABELS = {
24
24
  text: 'Text',
25
- url: 'Web address (link)',
25
+ url: 'URL',
26
26
  longtext: 'Long text',
27
27
  number: 'Number',
28
28
  boolean: 'Yes / No',
@@ -113,7 +113,7 @@
113
113
  : f.type === 'item' && linked[item.fields[f.key]]
114
114
  ? html`<a href="/items/${item.fields[f.key]}">${displayValue(f, item.fields[f.key], context)}</a>`
115
115
  : f.type === 'url'
116
- ? html`<a href=${item.fields[f.key]} rel="noopener">${item.fields[f.key]}</a>`
116
+ ? html`<a href=${item.fields[f.key]} target="_blank" rel="noopener">${item.fields[f.key]}</a>`
117
117
  : f.type === 'longtext'
118
118
  ? html`<span style="white-space: pre-wrap;">${item.fields[f.key]}</span>`
119
119
  : displayValue(f, item.fields[f.key], context)}</dd>`)}
@@ -24,7 +24,7 @@ export const MAX_IMAGE_EDGE = 8000;
24
24
  else, such as text to a number, could orphan existing values, so it is refused: delete the field
25
25
  and make a new one instead.
26
26
  */
27
- export const CONVERSIONS = { select: ['text'], text: ['longtext'] };
27
+ export const CONVERSIONS = { select: ['text'], text: ['longtext', 'url'] };
28
28
 
29
29
  export const canConvert = (from, to) => from === to || (CONVERSIONS[from] ?? []).includes(to);
30
30
 
@@ -3,7 +3,7 @@ import { eq, and, asc, sql } from 'drizzle-orm';
3
3
  import crypto from 'crypto';
4
4
  import { kempoInventoryField, kempoInventoryItem, kempoInventoryCategory } from '../db/schema.js';
5
5
  import { categoryKey, tidyCategory, MAX_CATEGORY } from './categoryLogic.js';
6
- import { normalizeFieldDefinition, OPTIONAL_TYPES, canConvert } from './fieldTypes.js';
6
+ import { normalizeFieldDefinition, OPTIONAL_TYPES, canConvert, coerceValue } from './fieldTypes.js';
7
7
  import { mediaAvailable } from './media.js';
8
8
  import { EVENTS, guard, notify } from './hooks.js';
9
9
 
@@ -11,6 +11,22 @@ const newId = () => crypto.randomBytes(8).toString('hex');
11
11
 
12
12
  const isUniqueViolation = error => (error?.code || error?.cause?.code) === '23505';
13
13
 
14
+ /*
15
+ The values items hold for a key, rewritten as full web addresses where they are one. Anything else is left
16
+ as it is (it still shows as text, and is not made into a link).
17
+ */
18
+ const addressesOf = async (key, label) => {
19
+ const rows = await db.select({ id: kempoInventoryItem.id, data: kempoInventoryItem.data }).from(kempoInventoryItem)
20
+ .where(sql`${kempoInventoryItem.data}->>${key} IS NOT NULL`);
21
+ const rewrites = [];
22
+ for(const row of rows){
23
+ const value = row.data[key];
24
+ if(value === '' || value === null) continue;
25
+ const [invalid, address] = coerceValue({ type: 'url', label }, value);
26
+ if(!invalid && address && address !== value) rewrites.push({ id: row.id, data: { ...row.data, [key]: address } });
27
+ }
28
+ return rewrites;
29
+ };
14
30
  /*
15
31
  Properties an extension's own field accepts on update, and what anyone else (the admin UI) may
16
32
  adjust on a field an extension owns. Key and ownership never change after creation, the scope
@@ -160,6 +176,9 @@ export const updateField = async (key, changes = {}, { owner = null, category =
160
176
  }
161
177
  if(!Object.keys(updates).length) return [{ code: 400, msg: 'No changes provided' }, null];
162
178
 
179
+ /* Text to a URL: values that are web addresses are kept as their full address; anything else stays text. */
180
+ const rewrites = targetType === 'url' && existing.type !== 'url' ? await addressesOf(key, existing.label) : [];
181
+
163
182
  const refused = await guard(EVENTS.fieldBeforeUpdate, { field: existing, changes: updates, actor: owner ?? '' });
164
183
  if(refused) return [refused, null];
165
184
 
@@ -172,6 +191,9 @@ export const updateField = async (key, changes = {}, { owner = null, category =
172
191
  .values({ id: newId(), key: targetScope, name: targetName, image: null, created: now, updated: now })
173
192
  .onConflictDoNothing();
174
193
  }
194
+ for(const rewrite of rewrites){
195
+ await tx.update(kempoInventoryItem).set({ data: rewrite.data }).where(eq(kempoInventoryItem.id, rewrite.id));
196
+ }
175
197
  const [updated] = await tx.update(kempoInventoryField).set(updates)
176
198
  .where(and(eq(kempoInventoryField.key, key), eq(kempoInventoryField.category, existing.category))).returning();
177
199
  return updated;