@ordinatio/entities 1.0.3 → 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 +4 -0
- package/dist/{chunk-QXBSEBJW.mjs → chunk-WWGAALKG.mjs} +32 -26
- package/dist/index.d.mts +1509 -0
- package/dist/index.mjs +51 -6
- package/dist/{observer-YUTEGG4S.mjs → observer-2GIY55TX.mjs} +1 -1
- package/dist/tags/index.d.mts +78 -0
- package/dist/tags/index.mjs +66 -0
- package/package.json +19 -21
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
evaluateConstraint,
|
|
10
10
|
fireObservers,
|
|
11
11
|
knowledgeError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-WWGAALKG.mjs";
|
|
13
13
|
|
|
14
14
|
// src/knowledge/field-definitions.ts
|
|
15
15
|
import { Prisma } from "@prisma/client";
|
|
@@ -199,7 +199,7 @@ async function setEntityFields(db, entityType, entityId, fields, source, sourceI
|
|
|
199
199
|
} catch {
|
|
200
200
|
}
|
|
201
201
|
try {
|
|
202
|
-
const { fireObservers: fireObservers2 } = await import("./observer-
|
|
202
|
+
const { fireObservers: fireObservers2 } = await import("./observer-2GIY55TX.mjs");
|
|
203
203
|
await fireObservers2(db, entityType, entityId, results.map((r) => r.key), callbacks);
|
|
204
204
|
} catch {
|
|
205
205
|
}
|
|
@@ -1668,6 +1668,7 @@ async function createNote(db, input, callbacks) {
|
|
|
1668
1668
|
isDraft: input.isDraft ?? false,
|
|
1669
1669
|
authorId: input.authorId ?? null,
|
|
1670
1670
|
source: input.source ?? "MANUAL",
|
|
1671
|
+
...input.metadata ? { metadata: input.metadata } : {},
|
|
1671
1672
|
...input.attachments && input.attachments.length > 0 ? {
|
|
1672
1673
|
attachments: {
|
|
1673
1674
|
create: input.attachments.map((a) => ({
|
|
@@ -1726,6 +1727,21 @@ async function updateNote(db, noteId, entityId, input, callbacks) {
|
|
|
1726
1727
|
});
|
|
1727
1728
|
if (!note) return null;
|
|
1728
1729
|
const isPublishing = note.isDraft && input.isDraft === false;
|
|
1730
|
+
const isContentChange = input.content !== void 0 && input.content !== note.content || input.contentHtml !== void 0 && input.contentHtml !== note.contentHtml;
|
|
1731
|
+
if (isContentChange && !note.isDraft) {
|
|
1732
|
+
try {
|
|
1733
|
+
await db.clientNoteRevision.create({
|
|
1734
|
+
data: {
|
|
1735
|
+
noteId: note.id,
|
|
1736
|
+
content: note.content,
|
|
1737
|
+
contentHtml: note.contentHtml,
|
|
1738
|
+
editedBy: input.editedBy ?? note.authorId,
|
|
1739
|
+
editedAt: /* @__PURE__ */ new Date()
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
} catch {
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1729
1745
|
const updated = await db.clientNote.update({
|
|
1730
1746
|
where: { id: noteId },
|
|
1731
1747
|
data: {
|
|
@@ -1735,7 +1751,11 @@ async function updateNote(db, noteId, entityId, input, callbacks) {
|
|
|
1735
1751
|
},
|
|
1736
1752
|
include: {
|
|
1737
1753
|
author: { select: { id: true, name: true } },
|
|
1738
|
-
attachments: true
|
|
1754
|
+
attachments: true,
|
|
1755
|
+
revisions: {
|
|
1756
|
+
orderBy: { editedAt: "desc" },
|
|
1757
|
+
select: { id: true, content: true, editedBy: true, editedAt: true }
|
|
1758
|
+
}
|
|
1739
1759
|
}
|
|
1740
1760
|
});
|
|
1741
1761
|
if (input.attachments !== void 0) {
|
|
@@ -1752,6 +1772,26 @@ async function updateNote(db, noteId, entityId, input, callbacks) {
|
|
|
1752
1772
|
});
|
|
1753
1773
|
}
|
|
1754
1774
|
}
|
|
1775
|
+
if (isContentChange && !note.isDraft && !isPublishing) {
|
|
1776
|
+
try {
|
|
1777
|
+
const noteContent = input.content ?? note.content;
|
|
1778
|
+
const truncated = truncateText(noteContent, 200);
|
|
1779
|
+
await callbacks?.logActivity?.(
|
|
1780
|
+
"CLIENT_NOTE_EDITED",
|
|
1781
|
+
`Note edited: ${truncated}`,
|
|
1782
|
+
{
|
|
1783
|
+
userId: input.editedBy ?? note.authorId ?? null,
|
|
1784
|
+
clientId: entityId,
|
|
1785
|
+
metadata: {
|
|
1786
|
+
noteId,
|
|
1787
|
+
contentPreview: truncateText(noteContent, 500),
|
|
1788
|
+
revisionCount: updated.revisions?.length ?? 0
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
);
|
|
1792
|
+
} catch {
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1755
1795
|
if (isPublishing) {
|
|
1756
1796
|
try {
|
|
1757
1797
|
const noteContent = input.content ?? note.content;
|
|
@@ -1776,7 +1816,11 @@ async function updateNote(db, noteId, entityId, input, callbacks) {
|
|
|
1776
1816
|
where: { id: noteId },
|
|
1777
1817
|
include: {
|
|
1778
1818
|
author: { select: { id: true, name: true } },
|
|
1779
|
-
attachments: true
|
|
1819
|
+
attachments: true,
|
|
1820
|
+
revisions: {
|
|
1821
|
+
orderBy: { editedAt: "desc" },
|
|
1822
|
+
select: { id: true, content: true, editedBy: true, editedAt: true }
|
|
1823
|
+
}
|
|
1780
1824
|
}
|
|
1781
1825
|
});
|
|
1782
1826
|
}
|
|
@@ -1960,6 +2004,7 @@ async function findOrCreateContact(db, email, name, source = "EMAIL_SYNC") {
|
|
|
1960
2004
|
}
|
|
1961
2005
|
|
|
1962
2006
|
// src/knowledge/contact-fields-seed.ts
|
|
2007
|
+
import { Prisma as Prisma4 } from "@prisma/client";
|
|
1963
2008
|
var CONTACT_FIELD_DEFINITIONS = [
|
|
1964
2009
|
// --- Communication ---
|
|
1965
2010
|
{ entityType: "contact", key: "preferred_contact_method", label: "Preferred Contact Method", dataType: "enum", category: "communication", enumOptions: ["email", "phone", "text", "in-person"], sortOrder: 0 },
|
|
@@ -1982,7 +2027,7 @@ async function seedContactFieldDefinitions(db) {
|
|
|
1982
2027
|
label: d.label,
|
|
1983
2028
|
dataType: d.dataType,
|
|
1984
2029
|
category: d.category,
|
|
1985
|
-
enumOptions: "enumOptions" in d ? d.enumOptions :
|
|
2030
|
+
enumOptions: "enumOptions" in d ? [...d.enumOptions] : Prisma4.JsonNull,
|
|
1986
2031
|
extractionHint: "extractionHint" in d ? d.extractionHint : null,
|
|
1987
2032
|
sortOrder: d.sortOrder,
|
|
1988
2033
|
status: "approved",
|
|
@@ -2119,7 +2164,7 @@ var CreateNoteSchema = z3.object({
|
|
|
2119
2164
|
content: z3.string().min(1, "Note content is required").max(5e3, "Note content must be 5000 characters or fewer"),
|
|
2120
2165
|
contentHtml: z3.string().max(5e4).optional(),
|
|
2121
2166
|
isDraft: z3.boolean().default(false),
|
|
2122
|
-
source: z3.enum(["MANUAL", "AGENT", "SYSTEM"]).default("MANUAL"),
|
|
2167
|
+
source: z3.enum(["MANUAL", "AGENT", "SYSTEM", "QUOTE"]).default("MANUAL"),
|
|
2123
2168
|
attachments: z3.array(AttachmentSchema).max(5, "Maximum 5 attachments").default([]),
|
|
2124
2169
|
fields: z3.record(z3.string(), z3.unknown()).optional()
|
|
2125
2170
|
});
|
|
@@ -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,44 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ordinatio/entities",
|
|
3
|
-
"version": "1.0
|
|
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": "./
|
|
11
|
-
"types": "./
|
|
10
|
+
"main": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
16
17
|
},
|
|
17
|
-
"./
|
|
18
|
-
|
|
18
|
+
"./tags": {
|
|
19
|
+
"types": "./dist/tags/index.d.mts",
|
|
20
|
+
"import": "./dist/tags/index.mjs",
|
|
21
|
+
"default": "./dist/tags/index.mjs"
|
|
22
|
+
}
|
|
19
23
|
},
|
|
20
24
|
"publishConfig": {
|
|
21
|
-
"access": "public"
|
|
22
|
-
"main": "./dist/index.js",
|
|
23
|
-
"types": "./dist/index.d.ts",
|
|
24
|
-
"exports": {
|
|
25
|
-
".": {
|
|
26
|
-
"import": "./dist/index.js",
|
|
27
|
-
"types": "./dist/index.d.ts"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
25
|
+
"access": "public"
|
|
30
26
|
},
|
|
31
27
|
"files": [
|
|
32
28
|
"dist",
|
|
33
29
|
"README.md",
|
|
34
30
|
"LICENSE"
|
|
35
31
|
],
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"test": "vitest",
|
|
39
|
-
"test:run": "vitest run"
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@ordinatio/errors": "1.2.0"
|
|
40
34
|
},
|
|
41
|
-
"dependencies": {},
|
|
42
35
|
"devDependencies": {
|
|
43
36
|
"@ordinatio/core": "^1.0.0",
|
|
44
37
|
"fast-check": "^4.5.3",
|
|
@@ -54,5 +47,10 @@
|
|
|
54
47
|
"@prisma/client": {
|
|
55
48
|
"optional": true
|
|
56
49
|
}
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup src/index.ts src/tags/index.ts --format esm --dts --clean",
|
|
53
|
+
"test": "vitest",
|
|
54
|
+
"test:run": "vitest run"
|
|
57
55
|
}
|
|
58
56
|
}
|