@fonderie/customers 6.1.6 → 6.2.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/brain/outcomes.md +2 -0
- package/dist/index.cjs +49 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +49 -25
- package/dist/index.js.map +1 -1
- package/dist/migrations/sql/013_label_workspace_scope.sql +22 -0
- package/package.json +1 -1
package/brain/outcomes.md
CHANGED
|
@@ -54,7 +54,9 @@ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
|
|
|
54
54
|
type fonderie_customer_label_type NOT NULL
|
|
55
55
|
value TEXT NOT NULL
|
|
56
56
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
57
|
+
workspace_id UUID
|
|
57
58
|
-- CONSTRAINT uq_fcl_type_value UNIQUE (type, value)
|
|
59
|
+
-- INDEX idx_fcl_workspace (workspace_id)
|
|
58
60
|
```
|
|
59
61
|
|
|
60
62
|
### `fonderie_customer_notes`
|
package/dist/index.cjs
CHANGED
|
@@ -1064,47 +1064,69 @@ var CustomerEmailModel = class {
|
|
|
1064
1064
|
};
|
|
1065
1065
|
|
|
1066
1066
|
// src/models/customer-label.model.ts
|
|
1067
|
+
var SELECT = `id, type, value, created_at AS "createdAt"`;
|
|
1067
1068
|
var CustomerLabelModel = class {
|
|
1068
1069
|
constructor(store) {
|
|
1069
1070
|
this.store = store;
|
|
1070
1071
|
}
|
|
1071
1072
|
store;
|
|
1072
|
-
|
|
1073
|
+
/**
|
|
1074
|
+
* List the labels a workspace may use: the shared/system defaults
|
|
1075
|
+
* (workspace_id IS NULL) plus its OWN private labels. Never another
|
|
1076
|
+
* workspace's — so listing can't enumerate other tenants' custom
|
|
1077
|
+
* vocabulary.
|
|
1078
|
+
*/
|
|
1079
|
+
list(type, workspaceId) {
|
|
1073
1080
|
return this.store.query(
|
|
1074
|
-
`SELECT
|
|
1081
|
+
`SELECT ${SELECT}
|
|
1075
1082
|
FROM fonderie_customer_labels
|
|
1076
|
-
WHERE type = $1
|
|
1083
|
+
WHERE type = $1 AND (workspace_id IS NULL OR workspace_id = $2)
|
|
1077
1084
|
ORDER BY value ASC`,
|
|
1078
|
-
[type]
|
|
1085
|
+
[type, workspaceId]
|
|
1079
1086
|
);
|
|
1080
1087
|
}
|
|
1081
1088
|
/**
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1089
|
+
* Delete a label. Only a label OWNED BY this workspace and UNREFERENCED can
|
|
1090
|
+
* go: shared defaults (workspace_id IS NULL) and other workspaces' labels
|
|
1091
|
+
* are untouchable, and an in-use label (still pointed at by an email/phone/
|
|
1092
|
+
* address) is refused so it can't be destroyed out from under a record.
|
|
1093
|
+
* Returns false when nothing was deleted.
|
|
1086
1094
|
*/
|
|
1087
|
-
async remove(id) {
|
|
1095
|
+
async remove(id, workspaceId) {
|
|
1088
1096
|
const rows = await this.store.query(
|
|
1089
1097
|
`DELETE FROM fonderie_customer_labels l
|
|
1090
1098
|
WHERE l.id = $1
|
|
1099
|
+
AND l.workspace_id = $2
|
|
1091
1100
|
AND NOT EXISTS (SELECT 1 FROM fonderie_customer_emails WHERE label_id = l.id)
|
|
1092
1101
|
AND NOT EXISTS (SELECT 1 FROM fonderie_customer_phones WHERE label_id = l.id)
|
|
1093
1102
|
AND NOT EXISTS (SELECT 1 FROM fonderie_customer_addresses WHERE label_id = l.id)
|
|
1094
1103
|
RETURNING l.id`,
|
|
1095
|
-
[id]
|
|
1104
|
+
[id, workspaceId]
|
|
1096
1105
|
);
|
|
1097
1106
|
return rows.length > 0;
|
|
1098
1107
|
}
|
|
1099
|
-
|
|
1108
|
+
/**
|
|
1109
|
+
* Resolve a (type, value) to a label id for this workspace: reuse a shared
|
|
1110
|
+
* default when one exists (so common labels like 'work' stay canonical),
|
|
1111
|
+
* otherwise create/reuse a label PRIVATE to this workspace.
|
|
1112
|
+
*/
|
|
1113
|
+
async findOrCreate(type, raw, workspaceId) {
|
|
1100
1114
|
const value = raw.trim().toLowerCase();
|
|
1101
|
-
const [
|
|
1102
|
-
`
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
RETURNING id, type, value, created_at AS "createdAt"`,
|
|
1115
|
+
const [shared] = await this.store.query(
|
|
1116
|
+
`SELECT ${SELECT} FROM fonderie_customer_labels
|
|
1117
|
+
WHERE type = $1 AND value = $2 AND workspace_id IS NULL
|
|
1118
|
+
LIMIT 1`,
|
|
1106
1119
|
[type, value]
|
|
1107
1120
|
);
|
|
1121
|
+
if (shared) return shared;
|
|
1122
|
+
const [row] = await this.store.query(
|
|
1123
|
+
`INSERT INTO fonderie_customer_labels (type, value, workspace_id)
|
|
1124
|
+
VALUES ($1, $2, $3)
|
|
1125
|
+
ON CONFLICT (type, value, workspace_id) WHERE workspace_id IS NOT NULL
|
|
1126
|
+
DO UPDATE SET value = EXCLUDED.value
|
|
1127
|
+
RETURNING ${SELECT}`,
|
|
1128
|
+
[type, value, workspaceId]
|
|
1129
|
+
);
|
|
1108
1130
|
if (!row) throw new Error("Failed to find or create label");
|
|
1109
1131
|
return row;
|
|
1110
1132
|
}
|
|
@@ -1711,7 +1733,7 @@ function customerAddressController(store) {
|
|
|
1711
1733
|
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "zipPostalCode is required");
|
|
1712
1734
|
}
|
|
1713
1735
|
const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "service";
|
|
1714
|
-
const { id: labelId } = await labels.findOrCreate("address", rawLabel);
|
|
1736
|
+
const { id: labelId } = await labels.findOrCreate("address", rawLabel, r.workspaceId);
|
|
1715
1737
|
let created;
|
|
1716
1738
|
try {
|
|
1717
1739
|
created = await addresses.add({
|
|
@@ -1749,7 +1771,7 @@ function customerAddressController(store) {
|
|
|
1749
1771
|
return (0, import_core3.setApiResponse)(import_core3.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
|
|
1750
1772
|
}
|
|
1751
1773
|
try {
|
|
1752
|
-
const { id: labelId } = await labels.findOrCreate("address", body["label"].trim());
|
|
1774
|
+
const { id: labelId } = await labels.findOrCreate("address", body["label"].trim(), r.workspaceId);
|
|
1753
1775
|
const updated = await addresses.updateLabel(addrId, r.customer.id, labelId);
|
|
1754
1776
|
return (0, import_core3.setApiResponse)(import_core3.HTTP.OK, "ADDRESS_UPDATED", "Address updated successfully.", {
|
|
1755
1777
|
address: toCustomerAddressDTO(updated)
|
|
@@ -1844,7 +1866,7 @@ function customerEmailController(store) {
|
|
|
1844
1866
|
}
|
|
1845
1867
|
const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "work";
|
|
1846
1868
|
const isPrimary2 = body?.["isPrimary"] === true;
|
|
1847
|
-
const { id: labelId } = await labels.findOrCreate("email", rawLabel);
|
|
1869
|
+
const { id: labelId } = await labels.findOrCreate("email", rawLabel, r.workspaceId);
|
|
1848
1870
|
let created;
|
|
1849
1871
|
try {
|
|
1850
1872
|
created = await emails.add({
|
|
@@ -1876,7 +1898,7 @@ function customerEmailController(store) {
|
|
|
1876
1898
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
|
|
1877
1899
|
}
|
|
1878
1900
|
try {
|
|
1879
|
-
const { id: labelId } = await labels.findOrCreate("email", body["label"].trim());
|
|
1901
|
+
const { id: labelId } = await labels.findOrCreate("email", body["label"].trim(), r.workspaceId);
|
|
1880
1902
|
const updated = await emails.updateLabel(emailId, r.customer.id, labelId);
|
|
1881
1903
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.OK, "EMAIL_UPDATED", "Email updated successfully.", {
|
|
1882
1904
|
email: toCustomerEmailDTO(updated)
|
|
@@ -2053,7 +2075,7 @@ function customerPhoneController(store) {
|
|
|
2053
2075
|
}
|
|
2054
2076
|
const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "mobile";
|
|
2055
2077
|
const isPrimary2 = body?.["isPrimary"] === true;
|
|
2056
|
-
const { id: labelId } = await labels.findOrCreate("phone", rawLabel);
|
|
2078
|
+
const { id: labelId } = await labels.findOrCreate("phone", rawLabel, r.workspaceId);
|
|
2057
2079
|
let created;
|
|
2058
2080
|
try {
|
|
2059
2081
|
created = await phones.add({
|
|
@@ -2085,7 +2107,7 @@ function customerPhoneController(store) {
|
|
|
2085
2107
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
|
|
2086
2108
|
}
|
|
2087
2109
|
try {
|
|
2088
|
-
const { id: labelId } = await labels.findOrCreate("phone", body["label"].trim());
|
|
2110
|
+
const { id: labelId } = await labels.findOrCreate("phone", body["label"].trim(), r.workspaceId);
|
|
2089
2111
|
const updated = await phones.updateLabel(phoneId, r.customer.id, labelId);
|
|
2090
2112
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.OK, "PHONE_UPDATED", "Phone updated successfully.", {
|
|
2091
2113
|
phone: toCustomerPhoneDTO(updated)
|
|
@@ -2196,7 +2218,8 @@ function customerLabelController(store) {
|
|
|
2196
2218
|
if (!type || !VALID_TYPES.includes(type)) {
|
|
2197
2219
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "type must be phone, email, or address");
|
|
2198
2220
|
}
|
|
2199
|
-
|
|
2221
|
+
if (!ctx.workspace) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
2222
|
+
const rows = await labels.list(type, ctx.workspace.id);
|
|
2200
2223
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "LABELS_FETCHED", "Labels retrieved successfully.", {
|
|
2201
2224
|
labels: rows.map(toCustomerLabelDTO)
|
|
2202
2225
|
});
|
|
@@ -2207,12 +2230,13 @@ function customerLabelController(store) {
|
|
|
2207
2230
|
if (!isUuid(labelId)) {
|
|
2208
2231
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.UNPROCESSABLE, "INVALID_PARAMETER", "labelId must be a valid UUID");
|
|
2209
2232
|
}
|
|
2210
|
-
|
|
2233
|
+
if (!ctx.workspace) return (0, import_core8.setApiResponse)(import_core8.HTTP.NOT_FOUND, "NOT_FOUND", "Workspace not found");
|
|
2234
|
+
const removed = await labels.remove(labelId, ctx.workspace.id);
|
|
2211
2235
|
if (!removed) {
|
|
2212
2236
|
return (0, import_core8.setApiResponse)(
|
|
2213
2237
|
import_core8.HTTP.CONFLICT,
|
|
2214
2238
|
"LABEL_IN_USE",
|
|
2215
|
-
"Label is
|
|
2239
|
+
"Label cannot be deleted \u2014 it is a shared default, not owned by this workspace, or still referenced."
|
|
2216
2240
|
);
|
|
2217
2241
|
}
|
|
2218
2242
|
return (0, import_core8.setApiResponse)(import_core8.HTTP.OK, "LABEL_DELETED", "Label deleted successfully.");
|