@fabriktor/fx 0.0.143 → 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.
- package/dist/src/chat/chat.d.ts +1 -1
- package/dist/src/chat/chat.js +20 -2
- package/dist/src/routes/availability.d.ts +1 -0
- package/dist/src/routes/availability.js +10 -2
- package/dist/src/routes/workspace.d.ts +1 -0
- package/dist/src/routes/workspace.js +1 -0
- package/dist/src/users/users.d.ts +9 -0
- package/dist/src/users/users.js +46 -2
- package/package.json +1 -1
package/dist/src/chat/chat.d.ts
CHANGED
|
@@ -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[];
|
package/dist/src/chat/chat.js
CHANGED
|
@@ -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
|
|
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) {
|
|
@@ -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)
|
|
@@ -91,6 +91,7 @@ export class RoutesWorkspace {
|
|
|
91
91
|
const query = {
|
|
92
92
|
...queryParams({
|
|
93
93
|
owner_id,
|
|
94
|
+
start_place_id: opts.start_place_id?.trim() || undefined,
|
|
94
95
|
// A day filter refers to the calendar day stored on each route.
|
|
95
96
|
// Employers may operate depots in different timezones.
|
|
96
97
|
utilized_employee_ids: actor.active_mode === UserTypeEmployee
|
|
@@ -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>;
|
package/dist/src/users/users.js
CHANGED
|
@@ -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
|
|
228
|
+
if (!user || user.id !== id || user.account_identity !== UserTypeExternal) {
|
|
185
229
|
throw errResolveFailed({
|
|
186
230
|
resource: resourceUser,
|
|
187
231
|
msg: msgExternalUserMissing,
|