@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/dist/index.js CHANGED
@@ -1028,47 +1028,69 @@ var CustomerEmailModel = class {
1028
1028
  };
1029
1029
 
1030
1030
  // src/models/customer-label.model.ts
1031
+ var SELECT = `id, type, value, created_at AS "createdAt"`;
1031
1032
  var CustomerLabelModel = class {
1032
1033
  constructor(store) {
1033
1034
  this.store = store;
1034
1035
  }
1035
1036
  store;
1036
- list(type) {
1037
+ /**
1038
+ * List the labels a workspace may use: the shared/system defaults
1039
+ * (workspace_id IS NULL) plus its OWN private labels. Never another
1040
+ * workspace's — so listing can't enumerate other tenants' custom
1041
+ * vocabulary.
1042
+ */
1043
+ list(type, workspaceId) {
1037
1044
  return this.store.query(
1038
- `SELECT id, type, value, created_at AS "createdAt"
1045
+ `SELECT ${SELECT}
1039
1046
  FROM fonderie_customer_labels
1040
- WHERE type = $1
1047
+ WHERE type = $1 AND (workspace_id IS NULL OR workspace_id = $2)
1041
1048
  ORDER BY value ASC`,
1042
- [type]
1049
+ [type, workspaceId]
1043
1050
  );
1044
1051
  }
1045
1052
  /**
1046
- * Returns false when the label is still referenced. Labels are a SHARED
1047
- * table (no workspace column), so an unconditional delete would let one
1048
- * tenant destroy a label other tenants' emails/phones/addresses point at.
1049
- * Until labels are workspace-scoped, only an unreferenced label may go.
1053
+ * Delete a label. Only a label OWNED BY this workspace and UNREFERENCED can
1054
+ * go: shared defaults (workspace_id IS NULL) and other workspaces' labels
1055
+ * are untouchable, and an in-use label (still pointed at by an email/phone/
1056
+ * address) is refused so it can't be destroyed out from under a record.
1057
+ * Returns false when nothing was deleted.
1050
1058
  */
1051
- async remove(id) {
1059
+ async remove(id, workspaceId) {
1052
1060
  const rows = await this.store.query(
1053
1061
  `DELETE FROM fonderie_customer_labels l
1054
1062
  WHERE l.id = $1
1063
+ AND l.workspace_id = $2
1055
1064
  AND NOT EXISTS (SELECT 1 FROM fonderie_customer_emails WHERE label_id = l.id)
1056
1065
  AND NOT EXISTS (SELECT 1 FROM fonderie_customer_phones WHERE label_id = l.id)
1057
1066
  AND NOT EXISTS (SELECT 1 FROM fonderie_customer_addresses WHERE label_id = l.id)
1058
1067
  RETURNING l.id`,
1059
- [id]
1068
+ [id, workspaceId]
1060
1069
  );
1061
1070
  return rows.length > 0;
1062
1071
  }
1063
- async findOrCreate(type, raw) {
1072
+ /**
1073
+ * Resolve a (type, value) to a label id for this workspace: reuse a shared
1074
+ * default when one exists (so common labels like 'work' stay canonical),
1075
+ * otherwise create/reuse a label PRIVATE to this workspace.
1076
+ */
1077
+ async findOrCreate(type, raw, workspaceId) {
1064
1078
  const value = raw.trim().toLowerCase();
1065
- const [row] = await this.store.query(
1066
- `INSERT INTO fonderie_customer_labels (type, value)
1067
- VALUES ($1, $2)
1068
- ON CONFLICT (type, value) DO UPDATE SET value = EXCLUDED.value
1069
- RETURNING id, type, value, created_at AS "createdAt"`,
1079
+ const [shared] = await this.store.query(
1080
+ `SELECT ${SELECT} FROM fonderie_customer_labels
1081
+ WHERE type = $1 AND value = $2 AND workspace_id IS NULL
1082
+ LIMIT 1`,
1070
1083
  [type, value]
1071
1084
  );
1085
+ if (shared) return shared;
1086
+ const [row] = await this.store.query(
1087
+ `INSERT INTO fonderie_customer_labels (type, value, workspace_id)
1088
+ VALUES ($1, $2, $3)
1089
+ ON CONFLICT (type, value, workspace_id) WHERE workspace_id IS NOT NULL
1090
+ DO UPDATE SET value = EXCLUDED.value
1091
+ RETURNING ${SELECT}`,
1092
+ [type, value, workspaceId]
1093
+ );
1072
1094
  if (!row) throw new Error("Failed to find or create label");
1073
1095
  return row;
1074
1096
  }
@@ -1675,7 +1697,7 @@ function customerAddressController(store) {
1675
1697
  return setApiResponse2(HTTP2.UNPROCESSABLE, "INVALID_PARAMETER", "zipPostalCode is required");
1676
1698
  }
1677
1699
  const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "service";
1678
- const { id: labelId } = await labels.findOrCreate("address", rawLabel);
1700
+ const { id: labelId } = await labels.findOrCreate("address", rawLabel, r.workspaceId);
1679
1701
  let created;
1680
1702
  try {
1681
1703
  created = await addresses.add({
@@ -1713,7 +1735,7 @@ function customerAddressController(store) {
1713
1735
  return setApiResponse2(HTTP2.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
1714
1736
  }
1715
1737
  try {
1716
- const { id: labelId } = await labels.findOrCreate("address", body["label"].trim());
1738
+ const { id: labelId } = await labels.findOrCreate("address", body["label"].trim(), r.workspaceId);
1717
1739
  const updated = await addresses.updateLabel(addrId, r.customer.id, labelId);
1718
1740
  return setApiResponse2(HTTP2.OK, "ADDRESS_UPDATED", "Address updated successfully.", {
1719
1741
  address: toCustomerAddressDTO(updated)
@@ -1808,7 +1830,7 @@ function customerEmailController(store) {
1808
1830
  }
1809
1831
  const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "work";
1810
1832
  const isPrimary2 = body?.["isPrimary"] === true;
1811
- const { id: labelId } = await labels.findOrCreate("email", rawLabel);
1833
+ const { id: labelId } = await labels.findOrCreate("email", rawLabel, r.workspaceId);
1812
1834
  let created;
1813
1835
  try {
1814
1836
  created = await emails.add({
@@ -1840,7 +1862,7 @@ function customerEmailController(store) {
1840
1862
  return setApiResponse3(HTTP3.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
1841
1863
  }
1842
1864
  try {
1843
- const { id: labelId } = await labels.findOrCreate("email", body["label"].trim());
1865
+ const { id: labelId } = await labels.findOrCreate("email", body["label"].trim(), r.workspaceId);
1844
1866
  const updated = await emails.updateLabel(emailId, r.customer.id, labelId);
1845
1867
  return setApiResponse3(HTTP3.OK, "EMAIL_UPDATED", "Email updated successfully.", {
1846
1868
  email: toCustomerEmailDTO(updated)
@@ -2017,7 +2039,7 @@ function customerPhoneController(store) {
2017
2039
  }
2018
2040
  const rawLabel = typeof body?.["label"] === "string" ? body["label"] : "mobile";
2019
2041
  const isPrimary2 = body?.["isPrimary"] === true;
2020
- const { id: labelId } = await labels.findOrCreate("phone", rawLabel);
2042
+ const { id: labelId } = await labels.findOrCreate("phone", rawLabel, r.workspaceId);
2021
2043
  let created;
2022
2044
  try {
2023
2045
  created = await phones.add({
@@ -2049,7 +2071,7 @@ function customerPhoneController(store) {
2049
2071
  return setApiResponse5(HTTP5.UNPROCESSABLE, "INVALID_PARAMETER", "label is required");
2050
2072
  }
2051
2073
  try {
2052
- const { id: labelId } = await labels.findOrCreate("phone", body["label"].trim());
2074
+ const { id: labelId } = await labels.findOrCreate("phone", body["label"].trim(), r.workspaceId);
2053
2075
  const updated = await phones.updateLabel(phoneId, r.customer.id, labelId);
2054
2076
  return setApiResponse5(HTTP5.OK, "PHONE_UPDATED", "Phone updated successfully.", {
2055
2077
  phone: toCustomerPhoneDTO(updated)
@@ -2160,7 +2182,8 @@ function customerLabelController(store) {
2160
2182
  if (!type || !VALID_TYPES.includes(type)) {
2161
2183
  return setApiResponse7(HTTP7.UNPROCESSABLE, "INVALID_PARAMETER", "type must be phone, email, or address");
2162
2184
  }
2163
- const rows = await labels.list(type);
2185
+ if (!ctx.workspace) return setApiResponse7(HTTP7.NOT_FOUND, "NOT_FOUND", "Workspace not found");
2186
+ const rows = await labels.list(type, ctx.workspace.id);
2164
2187
  return setApiResponse7(HTTP7.OK, "LABELS_FETCHED", "Labels retrieved successfully.", {
2165
2188
  labels: rows.map(toCustomerLabelDTO)
2166
2189
  });
@@ -2171,12 +2194,13 @@ function customerLabelController(store) {
2171
2194
  if (!isUuid(labelId)) {
2172
2195
  return setApiResponse7(HTTP7.UNPROCESSABLE, "INVALID_PARAMETER", "labelId must be a valid UUID");
2173
2196
  }
2174
- const removed = await labels.remove(labelId);
2197
+ if (!ctx.workspace) return setApiResponse7(HTTP7.NOT_FOUND, "NOT_FOUND", "Workspace not found");
2198
+ const removed = await labels.remove(labelId, ctx.workspace.id);
2175
2199
  if (!removed) {
2176
2200
  return setApiResponse7(
2177
2201
  HTTP7.CONFLICT,
2178
2202
  "LABEL_IN_USE",
2179
- "Label is still referenced by customer records and cannot be deleted."
2203
+ "Label cannot be deleted \u2014 it is a shared default, not owned by this workspace, or still referenced."
2180
2204
  );
2181
2205
  }
2182
2206
  return setApiResponse7(HTTP7.OK, "LABEL_DELETED", "Label deleted successfully.");