@tallyui/connector-vendure 3.0.2 → 3.0.4
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 +10 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.js +83 -2
- package/package.json +4 -4
- package/src/__tests__/fixtures/customers/1a-customers-search-matches.json +43 -0
- package/src/__tests__/fixtures/customers/1b-customers-search-none.json +16 -0
- package/src/__tests__/fixtures/customers/2a-createCustomer-success.json +30 -0
- package/src/__tests__/fixtures/customers/2b-createCustomer-duplicate-email.json +20 -0
- package/src/__tests__/fixtures/customers/3-customer-with-addresses.json +49 -0
- package/src/customers.ts +76 -0
- package/src/index.ts +7 -0
package/README.md
CHANGED
|
@@ -5,3 +5,13 @@
|
|
|
5
5
|
Requests with expired or rejected credentials reject with `ConnectorUnauthorizedError`, defined in `@tallyui/core` and re-exported by this connector. `error.status` is `401` (`code: 'unauthorized'`, sign in again) or `403` (`code: 'forbidden'`, signed in but not allowed: the pull retries on the store schedule; don't sign out); a signed-out Vendure session is always `401`.
|
|
6
6
|
|
|
7
7
|
Sign-in itself rejects with `SignInError`.
|
|
8
|
+
|
|
9
|
+
## Customers
|
|
10
|
+
|
|
11
|
+
`searchCustomers`, `createCustomer` and `getCustomer` are online only and run in the session's channel, selected by the `vendure-token` header.
|
|
12
|
+
|
|
13
|
+
The till's administrator needs `ReadCustomer` to search and look up customers, and `CreateCustomer` to create them. Without these permissions, the calls reject with `ConnectorUnauthorizedError` status `403`.
|
|
14
|
+
|
|
15
|
+
A duplicate email rejects with `CustomerServiceError` code `invalid`. The vendurepos walk-in customer is left out of search results.
|
|
16
|
+
|
|
17
|
+
Vendure's `CreateCustomerInput` requires `firstName` and `lastName`; leaving them out fails GraphQL input validation (recorded on Vendure 3.7.3). So `createCustomer` with only an email sends both as `''`, which Vendure 3.7.3 accepts: it creates the customer with empty names, and the till shows its email as the name.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConnectorAuth, ProductTraits, CollectionSync, ReplicationAdapter, StockReconcileAdapter, SyncContext, StoreSettingsChoice, StoreSettings, TallyConnector } from '@tallyui/core';
|
|
1
|
+
import { ConnectorAuth, ProductTraits, CollectionSync, ReplicationAdapter, StockReconcileAdapter, SyncContext, StoreSettingsChoice, StoreSettings, CustomerInput, Customer, TallyConnector } from '@tallyui/core';
|
|
2
2
|
export { ConnectorUnauthorizedError } from '@tallyui/core';
|
|
3
3
|
import { RxJsonSchema } from 'rxdb';
|
|
4
4
|
|
|
@@ -101,6 +101,19 @@ declare function vendureGlobalStockSettings(context: SyncContext): Promise<{
|
|
|
101
101
|
outOfStockThreshold: number;
|
|
102
102
|
}>;
|
|
103
103
|
|
|
104
|
+
declare function toVendureCustomer(raw: {
|
|
105
|
+
id: string;
|
|
106
|
+
firstName?: string | null;
|
|
107
|
+
lastName?: string | null;
|
|
108
|
+
emailAddress?: string | null;
|
|
109
|
+
phoneNumber?: string | null;
|
|
110
|
+
}): Customer;
|
|
111
|
+
declare function searchVendureCustomers(context: SyncContext, query: string, options?: {
|
|
112
|
+
limit?: number;
|
|
113
|
+
}): Promise<Customer[]>;
|
|
114
|
+
declare function createVendureCustomer(context: SyncContext, input: CustomerInput): Promise<Customer>;
|
|
115
|
+
declare function getVendureCustomer(context: SyncContext, id: string): Promise<Customer | null>;
|
|
116
|
+
|
|
104
117
|
/**
|
|
105
118
|
* Vendure connector for Tally UI. Build one per store session, anew on each sign-in or store change:
|
|
106
119
|
* each instance owns its reconcile feed, whose queued work must never reach another store's database (#307).
|
|
@@ -138,4 +151,4 @@ declare const createVendureConnector: (options?: {
|
|
|
138
151
|
*/
|
|
139
152
|
declare const vendureConnector: TallyConnector;
|
|
140
153
|
|
|
141
|
-
export { VendureTimezoneConfigError, createVendureConnector, createVendureProductReplication, createVendureVariantFeedReplication, vendureAuth, vendureConnector, vendureGlobalStockSettings, vendureProductReplication, vendureProductSchema, vendureProductSync, vendureProductTraits, vendureSignIn, vendureStockReconcile, vendureStoreSettings };
|
|
154
|
+
export { VendureTimezoneConfigError, createVendureConnector, createVendureCustomer, createVendureProductReplication, createVendureVariantFeedReplication, getVendureCustomer, searchVendureCustomers, toVendureCustomer, vendureAuth, vendureConnector, vendureGlobalStockSettings, vendureProductReplication, vendureProductSchema, vendureProductSync, vendureProductTraits, vendureSignIn, vendureStockReconcile, vendureStoreSettings };
|
package/dist/index.js
CHANGED
|
@@ -871,6 +871,79 @@ var vendureStoreSettings = async (context, _choice) => {
|
|
|
871
871
|
};
|
|
872
872
|
};
|
|
873
873
|
|
|
874
|
+
// src/customers.ts
|
|
875
|
+
import { ConnectorUnauthorizedError as ConnectorUnauthorizedError5, CustomerServiceError } from "@tallyui/core";
|
|
876
|
+
var WALK_IN_EMAIL = "walk-in@vendurepos.local";
|
|
877
|
+
var SEARCH_QUERY = `query Search($q: String!, $take: Int!) {
|
|
878
|
+
customers(options: {
|
|
879
|
+
filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },
|
|
880
|
+
filterOperator: OR, take: $take, sort: { lastName: ASC }
|
|
881
|
+
}) {
|
|
882
|
+
totalItems
|
|
883
|
+
items { id firstName lastName emailAddress phoneNumber }
|
|
884
|
+
}
|
|
885
|
+
}`;
|
|
886
|
+
var CREATE_QUERY = `mutation Create($input: CreateCustomerInput!) {
|
|
887
|
+
createCustomer(input: $input) {
|
|
888
|
+
__typename
|
|
889
|
+
... on Customer { id firstName lastName emailAddress phoneNumber }
|
|
890
|
+
... on ErrorResult { errorCode message }
|
|
891
|
+
}
|
|
892
|
+
}`;
|
|
893
|
+
var GET_QUERY = `query One($id: ID!) {
|
|
894
|
+
customer(id: $id) { id firstName lastName emailAddress phoneNumber }
|
|
895
|
+
}`;
|
|
896
|
+
function toVendureCustomer(raw) {
|
|
897
|
+
const customer = { id: raw.id, name: raw.id };
|
|
898
|
+
for (const [key, value] of [
|
|
899
|
+
["firstName", raw.firstName],
|
|
900
|
+
["lastName", raw.lastName],
|
|
901
|
+
["email", raw.emailAddress],
|
|
902
|
+
["phone", raw.phoneNumber]
|
|
903
|
+
]) {
|
|
904
|
+
if (typeof value === "string" && value.length > 0) customer[key] = value;
|
|
905
|
+
}
|
|
906
|
+
customer.name = [customer.firstName, customer.lastName].filter(Boolean).join(" ") || customer.email || customer.id;
|
|
907
|
+
return customer;
|
|
908
|
+
}
|
|
909
|
+
async function searchVendureCustomers(context, query2, options) {
|
|
910
|
+
const q = query2.trim();
|
|
911
|
+
if (!q) return [];
|
|
912
|
+
const take = Math.max(1, Math.min(50, options?.limit ?? 20));
|
|
913
|
+
const body = await customerGql(context, SEARCH_QUERY, { q, take });
|
|
914
|
+
const items = body?.data?.customers?.items;
|
|
915
|
+
if (!Array.isArray(items)) throw new CustomerServiceError("server", "unexpected response");
|
|
916
|
+
return items.filter((raw) => raw.emailAddress !== WALK_IN_EMAIL).map(toVendureCustomer);
|
|
917
|
+
}
|
|
918
|
+
async function createVendureCustomer(context, input) {
|
|
919
|
+
const variables = { input: {
|
|
920
|
+
emailAddress: input.email,
|
|
921
|
+
firstName: input.firstName ?? "",
|
|
922
|
+
lastName: input.lastName ?? "",
|
|
923
|
+
...input.phone ? { phoneNumber: input.phone } : {}
|
|
924
|
+
} };
|
|
925
|
+
const body = await customerGql(context, CREATE_QUERY, variables);
|
|
926
|
+
const result = body?.data?.createCustomer;
|
|
927
|
+
if (result?.__typename === "Customer") return toVendureCustomer(result);
|
|
928
|
+
if (result?.__typename && result.message) throw new CustomerServiceError("invalid", result.message);
|
|
929
|
+
throw new CustomerServiceError("server", "unexpected response");
|
|
930
|
+
}
|
|
931
|
+
async function customerGql(context, query2, variables) {
|
|
932
|
+
try {
|
|
933
|
+
return await gql2(context, query2, variables);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
if (error instanceof ConnectorUnauthorizedError5) throw error;
|
|
936
|
+
throw new CustomerServiceError(error instanceof Error && error.message.startsWith("Vendure ") ? "server" : "network", error instanceof Error ? error.message : String(error));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async function getVendureCustomer(context, id) {
|
|
940
|
+
const body = await customerGql(context, GET_QUERY, { id });
|
|
941
|
+
const raw = body?.data?.customer;
|
|
942
|
+
if (raw === null) return null;
|
|
943
|
+
if (typeof raw?.id !== "string") throw new CustomerServiceError("server", "unexpected response");
|
|
944
|
+
return toVendureCustomer(raw);
|
|
945
|
+
}
|
|
946
|
+
|
|
874
947
|
// src/global-settings.ts
|
|
875
948
|
var GLOBAL_SETTINGS_QUERY = `
|
|
876
949
|
query GlobalStockSettings {
|
|
@@ -883,7 +956,7 @@ async function vendureGlobalStockSettings(context) {
|
|
|
883
956
|
}
|
|
884
957
|
|
|
885
958
|
// src/index.ts
|
|
886
|
-
import { ConnectorUnauthorizedError as
|
|
959
|
+
import { ConnectorUnauthorizedError as ConnectorUnauthorizedError6 } from "@tallyui/core";
|
|
887
960
|
var VENDURE_IDS_PER_REQUEST = 1e3;
|
|
888
961
|
var createVendureConnector = (options = {}) => {
|
|
889
962
|
const idFeed = createReconcileFeed({ fetchByIds: createFetchByIds(options.barcodeField) });
|
|
@@ -923,17 +996,25 @@ var createVendureConnector = (options = {}) => {
|
|
|
923
996
|
prices: { fetchPages: fetchPages2, fingerprint, enqueue: idFeed.enqueue, refetchBatchSize: VENDURE_IDS_PER_REQUEST }
|
|
924
997
|
},
|
|
925
998
|
storeSettings: vendureStoreSettings,
|
|
999
|
+
// Needs the administrator's ReadCustomer permission (and CreateCustomer to create).
|
|
1000
|
+
searchCustomers: searchVendureCustomers,
|
|
1001
|
+
createCustomer: createVendureCustomer,
|
|
1002
|
+
getCustomer: getVendureCustomer,
|
|
926
1003
|
// Re-reads a restored session's capabilities; with an API key and no sign-in, the only reader.
|
|
927
1004
|
capabilities: (context) => readVendureCapabilities(context.baseUrl, context.headers, { signal: context.signal })
|
|
928
1005
|
};
|
|
929
1006
|
};
|
|
930
1007
|
var vendureConnector = createVendureConnector();
|
|
931
1008
|
export {
|
|
932
|
-
|
|
1009
|
+
ConnectorUnauthorizedError6 as ConnectorUnauthorizedError,
|
|
933
1010
|
VendureTimezoneConfigError,
|
|
934
1011
|
createVendureConnector,
|
|
1012
|
+
createVendureCustomer,
|
|
935
1013
|
createVendureProductReplication,
|
|
936
1014
|
createVendureVariantFeedReplication,
|
|
1015
|
+
getVendureCustomer,
|
|
1016
|
+
searchVendureCustomers,
|
|
1017
|
+
toVendureCustomer,
|
|
937
1018
|
vendureAuth,
|
|
938
1019
|
vendureConnector,
|
|
939
1020
|
vendureGlobalStockSettings,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tallyui/connector-vendure",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vendure connector for Tally UI",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"directory": "connectors/vendure"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"@tallyui/core": "^3.0.
|
|
29
|
+
"@tallyui/core": "^3.0.4"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@tallyui/database": "3.0.
|
|
33
|
-
"@tallyui/pos": "3.0.
|
|
32
|
+
"@tallyui/database": "3.0.4",
|
|
33
|
+
"@tallyui/pos": "3.0.4",
|
|
34
34
|
"rxdb": "17.5.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"query": "query Search($q: String!, $take: Int!) {\n customers(options: {\n filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },\n filterOperator: OR, take: $take, sort: { lastName: ASC }\n }) {\n totalItems\n items { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n }\n}",
|
|
3
|
+
"variables": {
|
|
4
|
+
"q": "ada",
|
|
5
|
+
"take": 20
|
|
6
|
+
},
|
|
7
|
+
"httpStatus": 200,
|
|
8
|
+
"response": {
|
|
9
|
+
"data": {
|
|
10
|
+
"customers": {
|
|
11
|
+
"totalItems": 2,
|
|
12
|
+
"items": [
|
|
13
|
+
{
|
|
14
|
+
"id": "3",
|
|
15
|
+
"createdAt": "2026-10-06T01:13:57.519Z",
|
|
16
|
+
"updatedAt": "2026-10-06T01:13:57.519Z",
|
|
17
|
+
"title": null,
|
|
18
|
+
"firstName": "Adam",
|
|
19
|
+
"lastName": "Fixture",
|
|
20
|
+
"emailAddress": "adam.fixture@example.com",
|
|
21
|
+
"phoneNumber": null,
|
|
22
|
+
"customFields": {
|
|
23
|
+
"__entityId__": 3
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "2",
|
|
28
|
+
"createdAt": "2026-10-06T01:13:57.496Z",
|
|
29
|
+
"updatedAt": "2026-10-06T01:13:57.496Z",
|
|
30
|
+
"title": null,
|
|
31
|
+
"firstName": "Ada",
|
|
32
|
+
"lastName": "Lovelace",
|
|
33
|
+
"emailAddress": "ada.fixture@example.com",
|
|
34
|
+
"phoneNumber": "+44 20 7946 0001",
|
|
35
|
+
"customFields": {
|
|
36
|
+
"__entityId__": 2
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"query": "query Search($q: String!, $take: Int!) {\n customers(options: {\n filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },\n filterOperator: OR, take: $take, sort: { lastName: ASC }\n }) {\n totalItems\n items { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n }\n}",
|
|
3
|
+
"variables": {
|
|
4
|
+
"q": "zzqqxx",
|
|
5
|
+
"take": 20
|
|
6
|
+
},
|
|
7
|
+
"httpStatus": 200,
|
|
8
|
+
"response": {
|
|
9
|
+
"data": {
|
|
10
|
+
"customers": {
|
|
11
|
+
"totalItems": 0,
|
|
12
|
+
"items": []
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"query": "mutation Create($input: CreateCustomerInput!) {\n createCustomer(input: $input) {\n __typename\n ... on Customer { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n ... on ErrorResult { errorCode message }\n }\n}",
|
|
3
|
+
"variables": {
|
|
4
|
+
"input": {
|
|
5
|
+
"firstName": "Ada",
|
|
6
|
+
"lastName": "Lovelace",
|
|
7
|
+
"emailAddress": "ada.fixture@example.com",
|
|
8
|
+
"phoneNumber": "+44 20 7946 0001"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"httpStatus": 200,
|
|
12
|
+
"response": {
|
|
13
|
+
"data": {
|
|
14
|
+
"createCustomer": {
|
|
15
|
+
"__typename": "Customer",
|
|
16
|
+
"id": "2",
|
|
17
|
+
"createdAt": "2026-10-06T01:13:57.496Z",
|
|
18
|
+
"updatedAt": "2026-10-06T01:13:57.496Z",
|
|
19
|
+
"title": null,
|
|
20
|
+
"firstName": "Ada",
|
|
21
|
+
"lastName": "Lovelace",
|
|
22
|
+
"emailAddress": "ada.fixture@example.com",
|
|
23
|
+
"phoneNumber": "+44 20 7946 0001",
|
|
24
|
+
"customFields": {
|
|
25
|
+
"__entityId__": 2
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"query": "mutation Create($input: CreateCustomerInput!) {\n createCustomer(input: $input) {\n __typename\n ... on Customer { id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields }\n ... on ErrorResult { errorCode message }\n }\n}",
|
|
3
|
+
"variables": {
|
|
4
|
+
"input": {
|
|
5
|
+
"firstName": "Ada",
|
|
6
|
+
"lastName": "Again",
|
|
7
|
+
"emailAddress": "ada.fixture@example.com"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"httpStatus": 200,
|
|
11
|
+
"response": {
|
|
12
|
+
"data": {
|
|
13
|
+
"createCustomer": {
|
|
14
|
+
"__typename": "EmailAddressConflictError",
|
|
15
|
+
"errorCode": "EMAIL_ADDRESS_CONFLICT_ERROR",
|
|
16
|
+
"message": "The email address is not available."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"query": "query One($id: ID!) {\n customer(id: $id) {\n id createdAt updatedAt title firstName lastName emailAddress phoneNumber customFields\n addresses { id fullName company streetLine1 streetLine2 city province postalCode country { code name } phoneNumber defaultShippingAddress defaultBillingAddress }\n user { id identifier verified }\n groups { id name }\n }\n}",
|
|
3
|
+
"variables": {
|
|
4
|
+
"id": "2"
|
|
5
|
+
},
|
|
6
|
+
"httpStatus": 200,
|
|
7
|
+
"response": {
|
|
8
|
+
"data": {
|
|
9
|
+
"customer": {
|
|
10
|
+
"id": "2",
|
|
11
|
+
"createdAt": "2026-10-06T01:13:57.496Z",
|
|
12
|
+
"updatedAt": "2026-10-06T01:13:57.496Z",
|
|
13
|
+
"title": null,
|
|
14
|
+
"firstName": "Ada",
|
|
15
|
+
"lastName": "Lovelace",
|
|
16
|
+
"emailAddress": "ada.fixture@example.com",
|
|
17
|
+
"phoneNumber": "+44 20 7946 0001",
|
|
18
|
+
"customFields": {
|
|
19
|
+
"__entityId__": 2
|
|
20
|
+
},
|
|
21
|
+
"addresses": [
|
|
22
|
+
{
|
|
23
|
+
"id": "1",
|
|
24
|
+
"fullName": "Ada Lovelace",
|
|
25
|
+
"company": "",
|
|
26
|
+
"streetLine1": "12 Fixture Street",
|
|
27
|
+
"streetLine2": "",
|
|
28
|
+
"city": "London",
|
|
29
|
+
"province": "",
|
|
30
|
+
"postalCode": "N1 1AA",
|
|
31
|
+
"country": {
|
|
32
|
+
"code": "DE",
|
|
33
|
+
"name": "Germany"
|
|
34
|
+
},
|
|
35
|
+
"phoneNumber": "",
|
|
36
|
+
"defaultShippingAddress": true,
|
|
37
|
+
"defaultBillingAddress": true
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"user": {
|
|
41
|
+
"id": "2",
|
|
42
|
+
"identifier": "ada.fixture@example.com",
|
|
43
|
+
"verified": false
|
|
44
|
+
},
|
|
45
|
+
"groups": []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/customers.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ConnectorUnauthorizedError, CustomerServiceError, type Customer, type CustomerInput, type SyncContext } from '@tallyui/core';
|
|
2
|
+
import { gql } from './replication/products';
|
|
3
|
+
|
|
4
|
+
// The vendurepos plugin's walk-in customer is never a person to attach to a sale.
|
|
5
|
+
const WALK_IN_EMAIL = 'walk-in@vendurepos.local';
|
|
6
|
+
|
|
7
|
+
const SEARCH_QUERY = `query Search($q: String!, $take: Int!) {
|
|
8
|
+
customers(options: {
|
|
9
|
+
filter: { emailAddress: { contains: $q }, firstName: { contains: $q }, lastName: { contains: $q } },
|
|
10
|
+
filterOperator: OR, take: $take, sort: { lastName: ASC }
|
|
11
|
+
}) {
|
|
12
|
+
totalItems
|
|
13
|
+
items { id firstName lastName emailAddress phoneNumber }
|
|
14
|
+
}
|
|
15
|
+
}`;
|
|
16
|
+
const CREATE_QUERY = `mutation Create($input: CreateCustomerInput!) {
|
|
17
|
+
createCustomer(input: $input) {
|
|
18
|
+
__typename
|
|
19
|
+
... on Customer { id firstName lastName emailAddress phoneNumber }
|
|
20
|
+
... on ErrorResult { errorCode message }
|
|
21
|
+
}
|
|
22
|
+
}`;
|
|
23
|
+
const GET_QUERY = `query One($id: ID!) {
|
|
24
|
+
customer(id: $id) { id firstName lastName emailAddress phoneNumber }
|
|
25
|
+
}`;
|
|
26
|
+
|
|
27
|
+
export function toVendureCustomer(raw: { id: string; firstName?: string | null; lastName?: string | null; emailAddress?: string | null; phoneNumber?: string | null }): Customer {
|
|
28
|
+
const customer: Customer = { id: raw.id, name: raw.id };
|
|
29
|
+
for (const [key, value] of [
|
|
30
|
+
['firstName', raw.firstName], ['lastName', raw.lastName],
|
|
31
|
+
['email', raw.emailAddress], ['phone', raw.phoneNumber],
|
|
32
|
+
] as const) {
|
|
33
|
+
if (typeof value === 'string' && value.length > 0) customer[key] = value;
|
|
34
|
+
}
|
|
35
|
+
customer.name = [customer.firstName, customer.lastName].filter(Boolean).join(' ') || customer.email || customer.id;
|
|
36
|
+
return customer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function searchVendureCustomers(context: SyncContext, query: string, options?: { limit?: number }): Promise<Customer[]> {
|
|
40
|
+
const q = query.trim();
|
|
41
|
+
if (!q) return [];
|
|
42
|
+
const take = Math.max(1, Math.min(50, options?.limit ?? 20));
|
|
43
|
+
const body = await customerGql(context, SEARCH_QUERY, { q, take });
|
|
44
|
+
const items = body?.data?.customers?.items;
|
|
45
|
+
if (!Array.isArray(items)) throw new CustomerServiceError('server', 'unexpected response');
|
|
46
|
+
return items.filter((raw) => raw.emailAddress !== WALK_IN_EMAIL).map(toVendureCustomer);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function createVendureCustomer(context: SyncContext, input: CustomerInput): Promise<Customer> {
|
|
50
|
+
const variables = { input: {
|
|
51
|
+
emailAddress: input.email, firstName: input.firstName ?? '', lastName: input.lastName ?? '',
|
|
52
|
+
...(input.phone ? { phoneNumber: input.phone } : {}),
|
|
53
|
+
} };
|
|
54
|
+
const body = await customerGql(context, CREATE_QUERY, variables);
|
|
55
|
+
const result = body?.data?.createCustomer;
|
|
56
|
+
if (result?.__typename === 'Customer') return toVendureCustomer(result);
|
|
57
|
+
if (result?.__typename && result.message) throw new CustomerServiceError('invalid', result.message);
|
|
58
|
+
throw new CustomerServiceError('server', 'unexpected response');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function customerGql(context: SyncContext, query: string, variables: Record<string, unknown>): Promise<any> {
|
|
62
|
+
try {
|
|
63
|
+
return await gql(context, query, variables);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof ConnectorUnauthorizedError) throw error;
|
|
66
|
+
throw new CustomerServiceError(error instanceof Error && error.message.startsWith('Vendure ') ? 'server' : 'network', error instanceof Error ? error.message : String(error));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getVendureCustomer(context: SyncContext, id: string): Promise<Customer | null> {
|
|
71
|
+
const body = await customerGql(context, GET_QUERY, { id });
|
|
72
|
+
const raw = body?.data?.customer;
|
|
73
|
+
if (raw === null) return null;
|
|
74
|
+
if (typeof raw?.id !== 'string') throw new CustomerServiceError('server', 'unexpected response');
|
|
75
|
+
return toVendureCustomer(raw);
|
|
76
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { createFetchByIds, fetchPages, variantIds } from './reconcile/ids';
|
|
|
12
12
|
import { fetchPages as fetchPricePages, fingerprint as priceFingerprint } from './reconcile/prices';
|
|
13
13
|
import { vendureStoreSettings } from './store-settings';
|
|
14
14
|
import { vendureGlobalStockSettings } from './global-settings';
|
|
15
|
+
import { searchVendureCustomers, createVendureCustomer, getVendureCustomer } from './customers';
|
|
15
16
|
|
|
16
17
|
/** Ids per fetchByIds request: it sends the reconcile feed's whole chunk (at most 1,000 ids, ADR-060) in one query. */
|
|
17
18
|
const VENDURE_IDS_PER_REQUEST = 1000;
|
|
@@ -86,6 +87,11 @@ export const createVendureConnector = (options: {
|
|
|
86
87
|
|
|
87
88
|
storeSettings: vendureStoreSettings,
|
|
88
89
|
|
|
90
|
+
// Needs the administrator's ReadCustomer permission (and CreateCustomer to create).
|
|
91
|
+
searchCustomers: searchVendureCustomers,
|
|
92
|
+
createCustomer: createVendureCustomer,
|
|
93
|
+
getCustomer: getVendureCustomer,
|
|
94
|
+
|
|
89
95
|
// Re-reads a restored session's capabilities; with an API key and no sign-in, the only reader.
|
|
90
96
|
capabilities: (context) => readVendureCapabilities(context.baseUrl, context.headers, { signal: context.signal }),
|
|
91
97
|
};
|
|
@@ -107,6 +113,7 @@ export { createVendureVariantFeedReplication } from './replication/variant-feed'
|
|
|
107
113
|
export { vendureStockReconcile } from './reconcile/stock';
|
|
108
114
|
export { vendureStoreSettings } from './store-settings';
|
|
109
115
|
export { vendureGlobalStockSettings } from './global-settings';
|
|
116
|
+
export { searchVendureCustomers, createVendureCustomer, getVendureCustomer, toVendureCustomer } from './customers';
|
|
110
117
|
|
|
111
118
|
export { ConnectorUnauthorizedError } from '@tallyui/core';
|
|
112
119
|
export { VendureTimezoneConfigError } from './replication/products';
|