@fabriktor/fx 0.0.144 → 0.0.145

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.
@@ -65,7 +65,7 @@ export type MyChatRoomMembersOptions = {
65
65
  export type ChatRoomParticipantsOptions = {
66
66
  chat_room_ids: string[];
67
67
  };
68
- export type ChatRoomParticipant = Pick<User, "id" | "full_name" | "username">;
68
+ export type ChatRoomParticipant = Pick<User, "id" | "full_name" | "username" | "account_identity">;
69
69
  export type ChatRoomParticipants = {
70
70
  members: ChatRoomMember[];
71
71
  users: ChatRoomParticipant[];
@@ -6,8 +6,9 @@
6
6
  import { queryParam, queryParams, } from "@fabriktor/client";
7
7
  import { IDKey, OwnerIDKey, } from "@fabriktor/schema";
8
8
  import { errRequired, errResolveFailed, errValidation } from "../core/err.js";
9
- import { requiredOperationID } from "../core/id.js";
9
+ import { requiredOperationID, normalizeIDs } from "../core/id.js";
10
10
  import { normalizeSearchPage, normalizeSearchPerPage, normalizeSearchQuery, runSearch, } from "../core/search.js";
11
+ import { isFabriktorUser } from "../users/users.js";
11
12
  import { ChatSendQueue } from "./send-queue.js";
12
13
  const closeCodeNormal = 1000;
13
14
  const wsRefreshMilliseconds = 40 * 60 * 1000;
@@ -19,6 +20,7 @@ const msgEnsuredChatRoomIDMissing = "Ensured chat room response did not contain
19
20
  const msgChatMissing = "Chat could not be resolved after the mutation.";
20
21
  const msgSearchQueryRequired = "Search query is required.";
21
22
  const msgChatObjectsUploading = "Cannot react to a chat while objects are still uploading.";
23
+ const msgChatRecipients = "Chat recipients must have a Fabriktor account. External contacts cannot receive messages or forms.";
22
24
  const msgParticipantMissing = "Chat room participant could not be resolved.";
23
25
  export class ChatFX {
24
26
  chat;
@@ -36,7 +38,22 @@ export class ChatFX {
36
38
  return await this.findChat(id);
37
39
  }
38
40
  async ensureChatRoom(opts) {
39
- const out = await this.chat.ensureChatRoom(opts);
41
+ const members = normalizeIDs(opts.members);
42
+ if (!members.length)
43
+ throw errRequired({
44
+ field: "members",
45
+ msg: msgParticipantMissing,
46
+ });
47
+ const users = await Promise.all(members.map(async (id) => {
48
+ const user = (await this.users.findOneUser({ id })).value;
49
+ return user?.id === id ? user : undefined;
50
+ }));
51
+ if (users.some((user) => !isFabriktorUser(user))) {
52
+ throw errValidation({
53
+ msg: msgChatRecipients,
54
+ });
55
+ }
56
+ const out = await this.chat.ensureChatRoom({ ...opts, members });
40
57
  return requiredOperationID(out, msgEnsuredChatRoomIDMissing);
41
58
  }
42
59
  async markChatObjectsUploaded(opts) {
@@ -456,6 +473,7 @@ export class ChatFX {
456
473
  id: user.id,
457
474
  full_name: user.full_name,
458
475
  username: user.username,
476
+ account_identity: user.account_identity,
459
477
  };
460
478
  }
461
479
  async findChat(id, signal) {
@@ -14,6 +14,7 @@ export type AvailabilityReference = Pick<InClientAvailability, "employer_id" | "
14
14
  export type AvailabilityContext = {
15
15
  location: Location;
16
16
  employer: User;
17
+ client: User;
17
18
  company: PrefCompany;
18
19
  timezone: string;
19
20
  };
@@ -1,4 +1,5 @@
1
1
  import { UserTypeClient, UserTypeEmployer, } from "@fabriktor/schema";
2
+ import { isFabriktorUser } from "../users/users.js";
2
3
  import { addressTimezone } from "../core/address.js";
3
4
  import { hoursPerDay, minutesOfDay, minutesPerHour, sameCalendarDay, } from "../core/date.js";
4
5
  import { errValidation } from "../core/err.js";
@@ -15,6 +16,7 @@ const msgAvailabilityAccess = "This availability does not match the client and e
15
16
  const msgAvailabilityTimezone = "The location needs a valid saved address timezone.";
16
17
  const msgAvailabilityHours = "The employer's working hours must form a valid same-day window.";
17
18
  const msgAvailabilityWindows = "Provide enough distinct, non-overlapping availability windows within the employer's working hours.";
19
+ const msgAvailabilityClient = "Visit times can only be requested from clients with a Fabriktor account. Arrange external contacts’ visit times directly.";
18
20
  const msgAvailabilityData = "The availability request is invalid.";
19
21
  export function availabilityReference(data) {
20
22
  if (data.type !== RoutesChatDataType.AvailabilityRequest &&
@@ -74,8 +76,9 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
74
76
  ? actor.owner_id !== reference.client_id
75
77
  : !routeCanPlan(actor) || actor.owner_id !== reference.employer_id))
76
78
  throw errValidation({ msg: msgAvailabilityAccess });
77
- const [result, company] = await Promise.all([
79
+ const [result, clientResult, company] = await Promise.all([
78
80
  readers.users.findOneUser({ id: reference.employer_id, signal }),
81
+ readers.users.findOneUser({ id: reference.client_id, signal }),
79
82
  readers.preferences.getPrefCompany({ owner_id: reference.employer_id }),
80
83
  ]);
81
84
  const employer = result.value;
@@ -83,6 +86,11 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
83
86
  employer.id !== reference.employer_id ||
84
87
  employer.account_identity !== UserTypeEmployer)
85
88
  throw errValidation({ msg: msgAvailabilityAccess });
89
+ const client = clientResult.value;
90
+ if (!client || client.id !== reference.client_id || !isFabriktorUser(client))
91
+ throw errValidation({
92
+ msg: msgAvailabilityClient,
93
+ });
86
94
  const timezone = addressTimezone(location);
87
95
  if (!timezone)
88
96
  throw errValidation({ msg: msgAvailabilityTimezone });
@@ -90,7 +98,7 @@ export async function loadAvailabilityContext(readers, actor, reference, signal)
90
98
  if (![start, end].every((hour) => Number.isInteger(hour) && hour >= 0 && hour < hoursPerDay) ||
91
99
  start >= end)
92
100
  throw errValidation({ msg: msgAvailabilityHours });
93
- return { location, employer, company, timezone };
101
+ return { location, employer, client, company, timezone };
94
102
  }
95
103
  export function validateAvailabilityWindows(inputs, context, minimum = clientAvailabilityMinimumWindows) {
96
104
  if (inputs.length < minimum)
@@ -10,6 +10,13 @@ export type ExternalUserInput = Omit<InUser, "account_identity" | "active_mode"
10
10
  export type CreateExternalUserOptions = {
11
11
  in: ExternalUserInput;
12
12
  };
13
+ export type UpdateExternalUserOptions = {
14
+ id: string;
15
+ owner_id: string;
16
+ in: ExternalUserInput;
17
+ };
18
+ export declare function isFabriktorUser(user: Pick<User, "account_identity"> | null | undefined): boolean;
19
+ export declare function canManageExternalUser(user: User | null | undefined, owner_id: string): boolean;
13
20
  export type SearchExternalUsersOptions = SearchUsersOptions;
14
21
  export type SearchExternalUsersPage = SearchUsersPage;
15
22
  export type UserProfileInput = Pick<InUser, "first_name" | "last_name" | "phone" | "company_name" | "privacy"> & InAddress;
@@ -43,6 +50,7 @@ export type UsersFXOptions = {
43
50
  export interface UsersFXOperator {
44
51
  searchUsers(opts: SearchUsersOptions): Promise<SearchUsersPage>;
45
52
  createExternalUser(opts: CreateExternalUserOptions): Promise<User>;
53
+ updateExternalUser(opts: UpdateExternalUserOptions): Promise<User>;
46
54
  searchExternalUsers(opts: SearchExternalUsersOptions): Promise<SearchExternalUsersPage>;
47
55
  updateUserProfile(opts: UpdateUserProfileOptions): Promise<User>;
48
56
  updateUserPhone(opts: UpdateUserPhoneOptions): Promise<User>;
@@ -61,6 +69,7 @@ export declare class UsersFX implements UsersFXOperator {
61
69
  constructor(opts: UsersFXOptions);
62
70
  searchUsers(opts: SearchUsersOptions): Promise<SearchUsersPage>;
63
71
  createExternalUser(opts: CreateExternalUserOptions): Promise<User>;
72
+ updateExternalUser(opts: UpdateExternalUserOptions): Promise<User>;
64
73
  searchExternalUsers(opts: SearchExternalUsersOptions): Promise<SearchExternalUsersPage>;
65
74
  updateUserProfile(opts: UpdateUserProfileOptions): Promise<User>;
66
75
  updateUserPhone(opts: UpdateUserPhoneOptions): Promise<User>;
@@ -5,7 +5,7 @@
5
5
  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
6
  import { UserTypeClient, UserTypeEmployee, UserTypeEmployer, UserTypeExternal, } from "@fabriktor/schema";
7
7
  import { errRequired, errResolveFailed, errValidation, } from "../core/err.js";
8
- import { requiredOperationID } from "../core/id.js";
8
+ import { requiredOperationID, resolvedID } from "../core/id.js";
9
9
  import { runSearch, } from "../core/search.js";
10
10
  import { normalizePhone, normalizePhoneCode, validatePhone, validatePhoneCode, } from "./phone.js";
11
11
  import { normalizeEmail, isValidEmail } from "./email.js";
@@ -20,12 +20,24 @@ const msgEmailInvalid = "Email address is invalid.";
20
20
  const msgAddressRequired = "A complete address is required.";
21
21
  const msgUserMissingAfterUpdate = "User could not be resolved after the account update.";
22
22
  const msgCreatedExternalUserIDMissing = "Created external user response did not contain an ID.";
23
+ const msgExternalUserAccess = "Only the owner can edit an external contact.";
23
24
  const msgExternalUserMissing = "External user could not be resolved after creation.";
24
25
  const msgInvalidActiveMode = "Active mode is not available for this account identity.";
25
26
  const msgWorkIdentityRequiresClient = "A work identity can only be established from a client account.";
26
27
  const msgInvalidWorkIdentity = "Work identity must be employee or employer.";
27
28
  const resourceUsername = "username";
28
29
  const resourceUser = "user";
30
+ // External records are private contacts, not messaging accounts.
31
+ export function isFabriktorUser(user) {
32
+ return (user?.account_identity === UserTypeClient ||
33
+ user?.account_identity === UserTypeEmployee ||
34
+ user?.account_identity === UserTypeEmployer);
35
+ }
36
+ export function canManageExternalUser(user, owner_id) {
37
+ return (!!resolvedID(owner_id) &&
38
+ user?.account_identity === UserTypeExternal &&
39
+ user.external_owner_id === owner_id);
40
+ }
29
41
  export class UsersFX {
30
42
  users;
31
43
  tmp_usernames;
@@ -62,6 +74,38 @@ export class UsersFX {
62
74
  const id = requiredOperationID(out, msgCreatedExternalUserIDMissing);
63
75
  return await this.findExternalUser(id);
64
76
  }
77
+ async updateExternalUser(opts) {
78
+ const current = await this.findExternalUser(opts.id);
79
+ if (!canManageExternalUser(current, opts.owner_id)) {
80
+ throw errValidation({
81
+ msg: msgExternalUserAccess,
82
+ });
83
+ }
84
+ const input = normalizeExternalUserInput(opts.in);
85
+ validateExternalUserInput(input);
86
+ // Only editable contact fields; preserve identity, privacy and account metadata.
87
+ await this.users.replaceOneUser({
88
+ id: current.id,
89
+ in: {
90
+ first_name: input.first_name,
91
+ last_name: input.last_name,
92
+ email: input.email,
93
+ phone: input.phone,
94
+ company_name: input.company_name,
95
+ lat: input.lat,
96
+ lon: input.lon,
97
+ street: input.street,
98
+ city: input.city,
99
+ state: input.state,
100
+ postal_code: input.postal_code,
101
+ country: input.country,
102
+ formatted_address: input.formatted_address,
103
+ place_name: input.place_name,
104
+ place_id: input.place_id,
105
+ },
106
+ });
107
+ return await this.findExternalUser(current.id);
108
+ }
65
109
  async searchExternalUsers(opts) {
66
110
  const { uid, ...searchOpts } = opts;
67
111
  return await runSearch({
@@ -181,7 +225,7 @@ export class UsersFX {
181
225
  async findExternalUser(id) {
182
226
  const out = await this.users.findOneUser({ id });
183
227
  const user = out.value;
184
- if (user === undefined) {
228
+ if (!user || user.id !== id || user.account_identity !== UserTypeExternal) {
185
229
  throw errResolveFailed({
186
230
  resource: resourceUser,
187
231
  msg: msgExternalUserMissing,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabriktor/fx",
3
- "version": "0.0.144",
3
+ "version": "0.0.145",
4
4
  "description": "![Fabriktor Logo](./img/fabriktor-character.gif)",
5
5
  "type": "module",
6
6
  "exports": {