@ordinatio/entities 1.0.4 → 1.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 CHANGED
@@ -191,6 +191,10 @@ await app.entities.createNote({ entityType: 'client', entityId: 'client-123', co
191
191
  | `getNotes(db, options)` | Query | List notes with cursor pagination |
192
192
  | `deleteNote(db, noteId, entityId)` | Mutation | Delete a note |
193
193
 
194
+ ### Tags Module (`@ordinatio/entities/tags`, experimental)
195
+
196
+ Pure, zero-dependency helpers for tags, on their own subpath so a browser component can import them without pulling in the rest of the package (no Prisma, zod or Node modules): `tagNameKey` / `cleanTagName` / `checkTagName` (the case-insensitive identity of a name; **pinned to its first shipped behaviour** because apps store the key in a unique column), `TAG_COLORS` / `isTagColor` / `resolveTagColor`, `DEFAULT_TAG_LIMITS` (name length 40, 50 changes per save, 15 pinned, 500 per workspace: defaults, a store takes them as arguments), and the picker helpers `describeTagChanges`, `tagSetKey`, `rebaseSelection`, `mergeTagOptions`. The `./schemas` and `./errors` subpaths exist only in the workspace (source) exports map and are not part of the published package yet. A store interface, service and contract suite are planned for a later release on a separate server entry; until a second product has used them, the interface is experimental.
197
+
194
198
  ### Contacts Module
195
199
 
196
200
  | Function | Type | Description |
@@ -0,0 +1,78 @@
1
+ declare const TAG_COLORS: readonly ["neutral", "red", "amber", "green", "blue", "purple", "pink"];
2
+ type TagColor = (typeof TAG_COLORS)[number];
3
+ interface TagLimits {
4
+ /** Longest tag name, in characters, after cleaning. */
5
+ maxNameLength: number;
6
+ /** Upper bound on how many tags one save may add or remove: stops a crafted request sending thousands of ids into an IN query. */
7
+ maxChangesPerSave: number;
8
+ /** How many tags may be pinned (shown as bookmarks): pinning more is refused rather than silently ignored. */
9
+ maxPinned: number;
10
+ /** Per-workspace ceiling: forms load every tag, so an unbounded vocabulary would slow every form. */
11
+ maxPerWorkspace: number;
12
+ }
13
+ declare const DEFAULT_TAG_LIMITS: Readonly<TagLimits>;
14
+
15
+ /** Trims and collapses every run of whitespace (including tabs and no-break spaces) to one plain space. */
16
+ declare function cleanTagName(name: string): string;
17
+ /** The identity of a name inside one workspace: the cleaned name, lower-cased. Two names with the same key are the same tag. */
18
+ declare function tagNameKey(name: string): string;
19
+ type TagNameCheck = {
20
+ ok: true;
21
+ name: string;
22
+ key: string;
23
+ } | {
24
+ ok: false;
25
+ reason: 'empty' | 'too-long';
26
+ length: number;
27
+ };
28
+ /** Cleans the name and checks its length (1 to `maxLength`, default 40, counted in UTF-16 code units like the stored value). */
29
+ declare function checkTagName(name: string, maxLength?: number): TagNameCheck;
30
+ declare function isTagColor(value: unknown): value is TagColor;
31
+ /** No colour means the neutral one; a name that is not in the list is invalid. */
32
+ declare function resolveTagColor(color: string | undefined): {
33
+ ok: true;
34
+ color: TagColor;
35
+ } | {
36
+ ok: false;
37
+ };
38
+
39
+ interface TagSnapshot {
40
+ name: string;
41
+ color: string;
42
+ pinned: boolean;
43
+ }
44
+ /** One phrase per field that actually changed; empty when nothing did (so nothing is logged). */
45
+ declare function describeTagChanges(before: TagSnapshot, after: TagSnapshot): string[];
46
+ /** Order-independent identity of a set of tag ids. */
47
+ declare function tagSetKey(ids: readonly string[]): string;
48
+ /**
49
+ * The server's tag set for an item changed while the user had the picker open (their own save came back, or a teammate tagged the item).
50
+ * Keep the user's unsaved edits (what they added and removed relative to the OLD baseline), apply them on top of the NEW server set, and
51
+ * make the new server set the baseline. So nothing the user chose is lost, nothing a teammate added is stripped, and the next save's diff
52
+ * stays correct.
53
+ */
54
+ interface RebasedSelection {
55
+ /** The new baseline: the server's current set, de-duplicated. */
56
+ initial: string[];
57
+ /** The user's unsaved choices applied on top of it. */
58
+ selected: string[];
59
+ }
60
+ declare function rebaseSelection(args: {
61
+ initial: readonly string[];
62
+ selected: readonly string[];
63
+ server: readonly string[];
64
+ }): RebasedSelection;
65
+ interface TagChoice {
66
+ id: string;
67
+ name: string;
68
+ color: string;
69
+ }
70
+ /**
71
+ * The tag list a picker shows: the server's current list (so renames, new tags and deletions by teammates show up), plus tags created in
72
+ * this very picker that the server list has not caught up with yet. Sorted by name with `localeCompare`, which follows the runtime's
73
+ * locale and ICU data, so a server and a browser can order accented or mixed-case names differently: do not rely on the order matching
74
+ * across runtimes.
75
+ */
76
+ declare function mergeTagOptions<T extends TagChoice>(fresh: readonly T[], createdHere: readonly T[]): T[];
77
+
78
+ export { DEFAULT_TAG_LIMITS, type RebasedSelection, TAG_COLORS, type TagChoice, type TagColor, type TagLimits, type TagNameCheck, type TagSnapshot, checkTagName, cleanTagName, describeTagChanges, isTagColor, mergeTagOptions, rebaseSelection, resolveTagColor, tagNameKey, tagSetKey };
@@ -0,0 +1,66 @@
1
+ // src/tags/constants.ts
2
+ var TAG_COLORS = ["neutral", "red", "amber", "green", "blue", "purple", "pink"];
3
+ var DEFAULT_TAG_LIMITS = Object.freeze({
4
+ maxNameLength: 40,
5
+ maxChangesPerSave: 50,
6
+ maxPinned: 15,
7
+ maxPerWorkspace: 500
8
+ });
9
+
10
+ // src/tags/name.ts
11
+ function cleanTagName(name) {
12
+ return name.trim().replace(/\s+/g, " ");
13
+ }
14
+ function tagNameKey(name) {
15
+ return cleanTagName(name).toLowerCase();
16
+ }
17
+ function checkTagName(name, maxLength = DEFAULT_TAG_LIMITS.maxNameLength) {
18
+ const cleaned = cleanTagName(name);
19
+ if (cleaned.length < 1) return { ok: false, reason: "empty", length: cleaned.length };
20
+ if (cleaned.length > maxLength) return { ok: false, reason: "too-long", length: cleaned.length };
21
+ return { ok: true, name: cleaned, key: tagNameKey(cleaned) };
22
+ }
23
+ function isTagColor(value) {
24
+ return typeof value === "string" && TAG_COLORS.includes(value);
25
+ }
26
+ function resolveTagColor(color) {
27
+ if (color === void 0) return { ok: true, color: "neutral" };
28
+ return isTagColor(color) ? { ok: true, color } : { ok: false };
29
+ }
30
+
31
+ // src/tags/changes.ts
32
+ function describeTagChanges(before, after) {
33
+ return [
34
+ before.name !== after.name ? `renamed "${before.name}" to "${after.name}"` : "",
35
+ before.color !== after.color ? `changed its color to ${after.color}` : "",
36
+ before.pinned !== after.pinned ? after.pinned ? "pinned it to Bookmarks" : "unpinned it from Bookmarks" : ""
37
+ ].filter(Boolean);
38
+ }
39
+ function tagSetKey(ids) {
40
+ return [...new Set(ids)].sort().join(",");
41
+ }
42
+ function rebaseSelection(args) {
43
+ const initial = new Set(args.initial);
44
+ const selected = new Set(args.selected);
45
+ const userAdded = [...selected].filter((t) => !initial.has(t));
46
+ const userRemoved = new Set([...initial].filter((t) => !selected.has(t)));
47
+ const next = new Set([...args.server, ...userAdded].filter((t) => !userRemoved.has(t)));
48
+ return { initial: [...new Set(args.server)], selected: [...next] };
49
+ }
50
+ function mergeTagOptions(fresh, createdHere) {
51
+ const freshIds = new Set(fresh.map((t) => t.id));
52
+ return [...fresh, ...createdHere.filter((t) => !freshIds.has(t.id))].sort((a, b) => a.name.localeCompare(b.name));
53
+ }
54
+ export {
55
+ DEFAULT_TAG_LIMITS,
56
+ TAG_COLORS,
57
+ checkTagName,
58
+ cleanTagName,
59
+ describeTagChanges,
60
+ isTagColor,
61
+ mergeTagOptions,
62
+ rebaseSelection,
63
+ resolveTagColor,
64
+ tagNameKey,
65
+ tagSetKey
66
+ };
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "name": "@ordinatio/entities",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Entity knowledge, agent intelligence, notes, and contacts",
7
7
  "engines": {
8
8
  "node": ">=22.0.0"
9
9
  },
10
- "main": "./dist/index.js",
11
- "types": "./dist/index.d.ts",
10
+ "main": "./dist/index.mjs",
11
+ "types": "./dist/index.d.mts",
12
12
  "exports": {
13
13
  ".": {
14
- "import": "./dist/index.js",
15
- "types": "./dist/index.d.ts"
14
+ "types": "./dist/index.d.mts",
15
+ "import": "./dist/index.mjs",
16
+ "default": "./dist/index.mjs"
17
+ },
18
+ "./tags": {
19
+ "types": "./dist/tags/index.d.mts",
20
+ "import": "./dist/tags/index.mjs",
21
+ "default": "./dist/tags/index.mjs"
16
22
  }
17
23
  },
18
24
  "publishConfig": {
@@ -24,7 +30,7 @@
24
30
  "LICENSE"
25
31
  ],
26
32
  "dependencies": {
27
- "@ordinatio/errors": "1.0.0"
33
+ "@ordinatio/errors": "1.2.0"
28
34
  },
29
35
  "devDependencies": {
30
36
  "@ordinatio/core": "^1.0.0",
@@ -43,7 +49,7 @@
43
49
  }
44
50
  },
45
51
  "scripts": {
46
- "build": "tsup src/index.ts --format esm --dts --clean",
52
+ "build": "tsup src/index.ts src/tags/index.ts --format esm --dts --clean",
47
53
  "test": "vitest",
48
54
  "test:run": "vitest run"
49
55
  }