@jytextiles/medusa-plugin-faire-store-sync 0.2.3 → 0.2.5
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/.medusa/server/src/admin/index.js +64 -9
- package/.medusa/server/src/admin/index.mjs +66 -11
- package/.medusa/server/src/modules/faire-sync/migrations/Migration20260709154411.js +18 -0
- package/.medusa/server/src/modules/faire-sync/migrations/Migration20260709161201.js +14 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-account.js +5 -1
- package/.medusa/server/src/modules/faire-sync/service.js +36 -15
- package/.medusa/server/src/workflows/ingest-faire-orders-bulk.js +2 -2
- package/.medusa/server/src/workflows/sync-product-to-faire.js +3 -2
- package/package.json +1 -1
- package/src/admin/routes/settings/faire/page.tsx +78 -8
- package/src/modules/faire-sync/migrations/.snapshot-medusa-faire-sync.json +25 -237
- package/src/modules/faire-sync/migrations/Migration20260709154411.ts +19 -0
- package/src/modules/faire-sync/migrations/Migration20260709161201.ts +13 -0
- package/src/modules/faire-sync/models/faire-sync-account.ts +4 -0
- package/src/modules/faire-sync/service.ts +39 -17
- package/src/workflows/ingest-faire-orders-bulk.ts +1 -1
- package/src/workflows/sync-product-to-faire.ts +3 -1
- package/.medusa/server/src/modules/faire-sync/models/faire-webhook-event.js +0 -21
- package/src/modules/faire-sync/models/faire-webhook-event.ts +0 -20
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
|
2
|
-
import { BuildingStorefront } from "@medusajs/icons"
|
|
2
|
+
import { BuildingStorefront, ChevronDownMini } from "@medusajs/icons"
|
|
3
3
|
import {
|
|
4
4
|
Alert,
|
|
5
5
|
Button,
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
DataTable,
|
|
8
8
|
DataTableFilteringState,
|
|
9
9
|
DataTablePaginationState,
|
|
10
|
+
Drawer,
|
|
11
|
+
DropdownMenu,
|
|
10
12
|
Heading,
|
|
13
|
+
Input,
|
|
11
14
|
Label,
|
|
12
15
|
Skeleton,
|
|
13
16
|
StatusBadge,
|
|
@@ -53,6 +56,8 @@ const FaireSettingsPage = () => {
|
|
|
53
56
|
pageSize: PAGE_SIZE,
|
|
54
57
|
})
|
|
55
58
|
const [filtering, setFiltering] = useState<DataTableFilteringState>({})
|
|
59
|
+
const [apiKeyOpen, setApiKeyOpen] = useState(false)
|
|
60
|
+
const [apiKey, setApiKey] = useState("")
|
|
56
61
|
|
|
57
62
|
// ── Queries ───────────────────────────────────────────────────────────────
|
|
58
63
|
const statusQuery = useQuery({
|
|
@@ -91,6 +96,20 @@ const FaireSettingsPage = () => {
|
|
|
91
96
|
}),
|
|
92
97
|
})
|
|
93
98
|
|
|
99
|
+
const apiKeyMutation = useMutation({
|
|
100
|
+
mutationFn: () => faireApi.connectApiKey(apiKey.trim()),
|
|
101
|
+
onSuccess: () => {
|
|
102
|
+
queryClient.invalidateQueries({ queryKey: STATUS_KEY })
|
|
103
|
+
setApiKeyOpen(false)
|
|
104
|
+
setApiKey("")
|
|
105
|
+
toast.success("Connected to Faire with API key")
|
|
106
|
+
},
|
|
107
|
+
onError: (err: any) =>
|
|
108
|
+
toast.error("Failed to connect with API key", {
|
|
109
|
+
description: err.message,
|
|
110
|
+
}),
|
|
111
|
+
})
|
|
112
|
+
|
|
94
113
|
const disconnectMutation = useMutation({
|
|
95
114
|
mutationFn: () => faireApi.disconnect(),
|
|
96
115
|
onSuccess: () => {
|
|
@@ -211,13 +230,22 @@ const FaireSettingsPage = () => {
|
|
|
211
230
|
</Button>
|
|
212
231
|
</div>
|
|
213
232
|
) : (
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
<DropdownMenu>
|
|
234
|
+
<DropdownMenu.Trigger asChild>
|
|
235
|
+
<Button size="small" isLoading={connectMutation.isPending}>
|
|
236
|
+
Connect Faire
|
|
237
|
+
<ChevronDownMini />
|
|
238
|
+
</Button>
|
|
239
|
+
</DropdownMenu.Trigger>
|
|
240
|
+
<DropdownMenu.Content>
|
|
241
|
+
<DropdownMenu.Item onClick={() => connectMutation.mutate()}>
|
|
242
|
+
Connect with OAuth
|
|
243
|
+
</DropdownMenu.Item>
|
|
244
|
+
<DropdownMenu.Item onClick={() => setApiKeyOpen(true)}>
|
|
245
|
+
Connect with API key (private)
|
|
246
|
+
</DropdownMenu.Item>
|
|
247
|
+
</DropdownMenu.Content>
|
|
248
|
+
</DropdownMenu>
|
|
221
249
|
)}
|
|
222
250
|
</div>
|
|
223
251
|
{connected && status?.account && (
|
|
@@ -264,6 +292,48 @@ const FaireSettingsPage = () => {
|
|
|
264
292
|
</DataTable>
|
|
265
293
|
</Container>
|
|
266
294
|
|
|
295
|
+
{/* API-key connect (private integrations) */}
|
|
296
|
+
<Drawer open={apiKeyOpen} onOpenChange={setApiKeyOpen}>
|
|
297
|
+
<Drawer.Content>
|
|
298
|
+
<Drawer.Header>
|
|
299
|
+
<Drawer.Title>Connect with API key</Drawer.Title>
|
|
300
|
+
</Drawer.Header>
|
|
301
|
+
<Drawer.Body className="flex flex-col gap-y-3">
|
|
302
|
+
<Text className="text-ui-fg-subtle" size="small">
|
|
303
|
+
For private / unpublished Faire integrations, paste the API key
|
|
304
|
+
your Faire brand issued for this app. This connects without OAuth.
|
|
305
|
+
</Text>
|
|
306
|
+
<div className="flex flex-col gap-y-1">
|
|
307
|
+
<Label size="small">Faire API key</Label>
|
|
308
|
+
<Input
|
|
309
|
+
type="password"
|
|
310
|
+
placeholder="Paste the Faire API key…"
|
|
311
|
+
value={apiKey}
|
|
312
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
313
|
+
autoComplete="off"
|
|
314
|
+
/>
|
|
315
|
+
</div>
|
|
316
|
+
</Drawer.Body>
|
|
317
|
+
<Drawer.Footer>
|
|
318
|
+
<Button
|
|
319
|
+
variant="secondary"
|
|
320
|
+
size="small"
|
|
321
|
+
onClick={() => setApiKeyOpen(false)}
|
|
322
|
+
>
|
|
323
|
+
Cancel
|
|
324
|
+
</Button>
|
|
325
|
+
<Button
|
|
326
|
+
size="small"
|
|
327
|
+
onClick={() => apiKeyMutation.mutate()}
|
|
328
|
+
isLoading={apiKeyMutation.isPending}
|
|
329
|
+
disabled={!apiKey.trim()}
|
|
330
|
+
>
|
|
331
|
+
Connect
|
|
332
|
+
</Button>
|
|
333
|
+
</Drawer.Footer>
|
|
334
|
+
</Drawer.Content>
|
|
335
|
+
</Drawer>
|
|
336
|
+
|
|
267
337
|
<Toaster />
|
|
268
338
|
</div>
|
|
269
339
|
)
|
|
@@ -301,6 +301,25 @@
|
|
|
301
301
|
"enumItems": [],
|
|
302
302
|
"mappedType": "text"
|
|
303
303
|
},
|
|
304
|
+
"auth_mode": {
|
|
305
|
+
"name": "auth_mode",
|
|
306
|
+
"type": "text",
|
|
307
|
+
"unsigned": false,
|
|
308
|
+
"autoincrement": false,
|
|
309
|
+
"primary": false,
|
|
310
|
+
"nullable": false,
|
|
311
|
+
"unique": false,
|
|
312
|
+
"length": null,
|
|
313
|
+
"precision": null,
|
|
314
|
+
"scale": null,
|
|
315
|
+
"default": "'oauth'",
|
|
316
|
+
"comment": null,
|
|
317
|
+
"enumItems": [
|
|
318
|
+
"oauth",
|
|
319
|
+
"apiKey"
|
|
320
|
+
],
|
|
321
|
+
"mappedType": "enum"
|
|
322
|
+
},
|
|
304
323
|
"access_token": {
|
|
305
324
|
"name": "access_token",
|
|
306
325
|
"type": "text",
|
|
@@ -1158,230 +1177,8 @@
|
|
|
1158
1177
|
"enumItems": [],
|
|
1159
1178
|
"mappedType": "json"
|
|
1160
1179
|
},
|
|
1161
|
-
"
|
|
1162
|
-
"name": "
|
|
1163
|
-
"type": "timestamptz",
|
|
1164
|
-
"unsigned": false,
|
|
1165
|
-
"autoincrement": false,
|
|
1166
|
-
"primary": false,
|
|
1167
|
-
"nullable": false,
|
|
1168
|
-
"unique": false,
|
|
1169
|
-
"length": 6,
|
|
1170
|
-
"precision": null,
|
|
1171
|
-
"scale": null,
|
|
1172
|
-
"default": "now()",
|
|
1173
|
-
"comment": null,
|
|
1174
|
-
"enumItems": [],
|
|
1175
|
-
"mappedType": "datetime"
|
|
1176
|
-
},
|
|
1177
|
-
"updated_at": {
|
|
1178
|
-
"name": "updated_at",
|
|
1179
|
-
"type": "timestamptz",
|
|
1180
|
-
"unsigned": false,
|
|
1181
|
-
"autoincrement": false,
|
|
1182
|
-
"primary": false,
|
|
1183
|
-
"nullable": false,
|
|
1184
|
-
"unique": false,
|
|
1185
|
-
"length": 6,
|
|
1186
|
-
"precision": null,
|
|
1187
|
-
"scale": null,
|
|
1188
|
-
"default": "now()",
|
|
1189
|
-
"comment": null,
|
|
1190
|
-
"enumItems": [],
|
|
1191
|
-
"mappedType": "datetime"
|
|
1192
|
-
},
|
|
1193
|
-
"deleted_at": {
|
|
1194
|
-
"name": "deleted_at",
|
|
1195
|
-
"type": "timestamptz",
|
|
1196
|
-
"unsigned": false,
|
|
1197
|
-
"autoincrement": false,
|
|
1198
|
-
"primary": false,
|
|
1199
|
-
"nullable": true,
|
|
1200
|
-
"unique": false,
|
|
1201
|
-
"length": 6,
|
|
1202
|
-
"precision": null,
|
|
1203
|
-
"scale": null,
|
|
1204
|
-
"default": null,
|
|
1205
|
-
"comment": null,
|
|
1206
|
-
"enumItems": [],
|
|
1207
|
-
"mappedType": "datetime"
|
|
1208
|
-
}
|
|
1209
|
-
},
|
|
1210
|
-
"name": "faire_sync_settings",
|
|
1211
|
-
"schema": "public",
|
|
1212
|
-
"indexes": [
|
|
1213
|
-
{
|
|
1214
|
-
"keyName": "IDX_faire_sync_settings_deleted_at",
|
|
1215
|
-
"columnNames": [],
|
|
1216
|
-
"composite": false,
|
|
1217
|
-
"constraint": false,
|
|
1218
|
-
"primary": false,
|
|
1219
|
-
"unique": false,
|
|
1220
|
-
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_faire_sync_settings_deleted_at\" ON \"faire_sync_settings\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
|
1221
|
-
},
|
|
1222
|
-
{
|
|
1223
|
-
"keyName": "faire_sync_settings_pkey",
|
|
1224
|
-
"columnNames": [
|
|
1225
|
-
"id"
|
|
1226
|
-
],
|
|
1227
|
-
"composite": false,
|
|
1228
|
-
"constraint": true,
|
|
1229
|
-
"primary": true,
|
|
1230
|
-
"unique": true
|
|
1231
|
-
}
|
|
1232
|
-
],
|
|
1233
|
-
"checks": [],
|
|
1234
|
-
"foreignKeys": {},
|
|
1235
|
-
"nativeEnums": {}
|
|
1236
|
-
},
|
|
1237
|
-
{
|
|
1238
|
-
"columns": {
|
|
1239
|
-
"id": {
|
|
1240
|
-
"name": "id",
|
|
1241
|
-
"type": "text",
|
|
1242
|
-
"unsigned": false,
|
|
1243
|
-
"autoincrement": false,
|
|
1244
|
-
"primary": true,
|
|
1245
|
-
"nullable": false,
|
|
1246
|
-
"unique": false,
|
|
1247
|
-
"length": null,
|
|
1248
|
-
"precision": null,
|
|
1249
|
-
"scale": null,
|
|
1250
|
-
"default": null,
|
|
1251
|
-
"comment": null,
|
|
1252
|
-
"enumItems": [],
|
|
1253
|
-
"mappedType": "text"
|
|
1254
|
-
},
|
|
1255
|
-
"webhook_id": {
|
|
1256
|
-
"name": "webhook_id",
|
|
1257
|
-
"type": "text",
|
|
1258
|
-
"unsigned": false,
|
|
1259
|
-
"autoincrement": false,
|
|
1260
|
-
"primary": false,
|
|
1261
|
-
"nullable": false,
|
|
1262
|
-
"unique": false,
|
|
1263
|
-
"length": null,
|
|
1264
|
-
"precision": null,
|
|
1265
|
-
"scale": null,
|
|
1266
|
-
"default": null,
|
|
1267
|
-
"comment": null,
|
|
1268
|
-
"enumItems": [],
|
|
1269
|
-
"mappedType": "text"
|
|
1270
|
-
},
|
|
1271
|
-
"event_type": {
|
|
1272
|
-
"name": "event_type",
|
|
1273
|
-
"type": "text",
|
|
1274
|
-
"unsigned": false,
|
|
1275
|
-
"autoincrement": false,
|
|
1276
|
-
"primary": false,
|
|
1277
|
-
"nullable": false,
|
|
1278
|
-
"unique": false,
|
|
1279
|
-
"length": null,
|
|
1280
|
-
"precision": null,
|
|
1281
|
-
"scale": null,
|
|
1282
|
-
"default": null,
|
|
1283
|
-
"comment": null,
|
|
1284
|
-
"enumItems": [],
|
|
1285
|
-
"mappedType": "text"
|
|
1286
|
-
},
|
|
1287
|
-
"brand_id": {
|
|
1288
|
-
"name": "brand_id",
|
|
1289
|
-
"type": "text",
|
|
1290
|
-
"unsigned": false,
|
|
1291
|
-
"autoincrement": false,
|
|
1292
|
-
"primary": false,
|
|
1293
|
-
"nullable": true,
|
|
1294
|
-
"unique": false,
|
|
1295
|
-
"length": null,
|
|
1296
|
-
"precision": null,
|
|
1297
|
-
"scale": null,
|
|
1298
|
-
"default": null,
|
|
1299
|
-
"comment": null,
|
|
1300
|
-
"enumItems": [],
|
|
1301
|
-
"mappedType": "text"
|
|
1302
|
-
},
|
|
1303
|
-
"resource_url": {
|
|
1304
|
-
"name": "resource_url",
|
|
1305
|
-
"type": "text",
|
|
1306
|
-
"unsigned": false,
|
|
1307
|
-
"autoincrement": false,
|
|
1308
|
-
"primary": false,
|
|
1309
|
-
"nullable": true,
|
|
1310
|
-
"unique": false,
|
|
1311
|
-
"length": null,
|
|
1312
|
-
"precision": null,
|
|
1313
|
-
"scale": null,
|
|
1314
|
-
"default": null,
|
|
1315
|
-
"comment": null,
|
|
1316
|
-
"enumItems": [],
|
|
1317
|
-
"mappedType": "text"
|
|
1318
|
-
},
|
|
1319
|
-
"payload": {
|
|
1320
|
-
"name": "payload",
|
|
1321
|
-
"type": "jsonb",
|
|
1322
|
-
"unsigned": false,
|
|
1323
|
-
"autoincrement": false,
|
|
1324
|
-
"primary": false,
|
|
1325
|
-
"nullable": true,
|
|
1326
|
-
"unique": false,
|
|
1327
|
-
"length": null,
|
|
1328
|
-
"precision": null,
|
|
1329
|
-
"scale": null,
|
|
1330
|
-
"default": null,
|
|
1331
|
-
"comment": null,
|
|
1332
|
-
"enumItems": [],
|
|
1333
|
-
"mappedType": "json"
|
|
1334
|
-
},
|
|
1335
|
-
"resource": {
|
|
1336
|
-
"name": "resource",
|
|
1337
|
-
"type": "jsonb",
|
|
1338
|
-
"unsigned": false,
|
|
1339
|
-
"autoincrement": false,
|
|
1340
|
-
"primary": false,
|
|
1341
|
-
"nullable": true,
|
|
1342
|
-
"unique": false,
|
|
1343
|
-
"length": null,
|
|
1344
|
-
"precision": null,
|
|
1345
|
-
"scale": null,
|
|
1346
|
-
"default": null,
|
|
1347
|
-
"comment": null,
|
|
1348
|
-
"enumItems": [],
|
|
1349
|
-
"mappedType": "json"
|
|
1350
|
-
},
|
|
1351
|
-
"processed": {
|
|
1352
|
-
"name": "processed",
|
|
1353
|
-
"type": "boolean",
|
|
1354
|
-
"unsigned": false,
|
|
1355
|
-
"autoincrement": false,
|
|
1356
|
-
"primary": false,
|
|
1357
|
-
"nullable": false,
|
|
1358
|
-
"unique": false,
|
|
1359
|
-
"length": null,
|
|
1360
|
-
"precision": null,
|
|
1361
|
-
"scale": null,
|
|
1362
|
-
"default": "false",
|
|
1363
|
-
"comment": null,
|
|
1364
|
-
"enumItems": [],
|
|
1365
|
-
"mappedType": "boolean"
|
|
1366
|
-
},
|
|
1367
|
-
"error": {
|
|
1368
|
-
"name": "error",
|
|
1369
|
-
"type": "text",
|
|
1370
|
-
"unsigned": false,
|
|
1371
|
-
"autoincrement": false,
|
|
1372
|
-
"primary": false,
|
|
1373
|
-
"nullable": true,
|
|
1374
|
-
"unique": false,
|
|
1375
|
-
"length": null,
|
|
1376
|
-
"precision": null,
|
|
1377
|
-
"scale": null,
|
|
1378
|
-
"default": null,
|
|
1379
|
-
"comment": null,
|
|
1380
|
-
"enumItems": [],
|
|
1381
|
-
"mappedType": "text"
|
|
1382
|
-
},
|
|
1383
|
-
"received_at": {
|
|
1384
|
-
"name": "received_at",
|
|
1180
|
+
"last_order_sync_at": {
|
|
1181
|
+
"name": "last_order_sync_at",
|
|
1385
1182
|
"type": "timestamptz",
|
|
1386
1183
|
"unsigned": false,
|
|
1387
1184
|
"autoincrement": false,
|
|
@@ -1445,29 +1242,20 @@
|
|
|
1445
1242
|
"mappedType": "datetime"
|
|
1446
1243
|
}
|
|
1447
1244
|
},
|
|
1448
|
-
"name": "
|
|
1245
|
+
"name": "faire_sync_settings",
|
|
1449
1246
|
"schema": "public",
|
|
1450
1247
|
"indexes": [
|
|
1451
1248
|
{
|
|
1452
|
-
"keyName": "
|
|
1453
|
-
"columnNames": [],
|
|
1454
|
-
"composite": false,
|
|
1455
|
-
"constraint": false,
|
|
1456
|
-
"primary": false,
|
|
1457
|
-
"unique": false,
|
|
1458
|
-
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_faire_webhook_event_webhook_id_unique\" ON \"faire_webhook_event\" (\"webhook_id\") WHERE deleted_at IS NULL"
|
|
1459
|
-
},
|
|
1460
|
-
{
|
|
1461
|
-
"keyName": "IDX_faire_webhook_event_deleted_at",
|
|
1249
|
+
"keyName": "IDX_faire_sync_settings_deleted_at",
|
|
1462
1250
|
"columnNames": [],
|
|
1463
1251
|
"composite": false,
|
|
1464
1252
|
"constraint": false,
|
|
1465
1253
|
"primary": false,
|
|
1466
1254
|
"unique": false,
|
|
1467
|
-
"expression": "CREATE INDEX IF NOT EXISTS \"
|
|
1255
|
+
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_faire_sync_settings_deleted_at\" ON \"faire_sync_settings\" (\"deleted_at\") WHERE deleted_at IS NULL"
|
|
1468
1256
|
},
|
|
1469
1257
|
{
|
|
1470
|
-
"keyName": "
|
|
1258
|
+
"keyName": "faire_sync_settings_pkey",
|
|
1471
1259
|
"columnNames": [
|
|
1472
1260
|
"id"
|
|
1473
1261
|
],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
|
|
2
|
+
|
|
3
|
+
export class Migration20260709154411 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`drop table if exists "faire_webhook_event" cascade;`);
|
|
7
|
+
|
|
8
|
+
this.addSql(`alter table if exists "faire_sync_settings" add column if not exists "last_order_sync_at" timestamptz null;`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
override async down(): Promise<void> {
|
|
12
|
+
this.addSql(`create table if not exists "faire_webhook_event" ("id" text not null, "webhook_id" text not null, "event_type" text not null, "brand_id" text null, "resource_url" text null, "payload" jsonb null, "resource" jsonb null, "processed" boolean not null default false, "error" text null, "received_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "faire_webhook_event_pkey" primary key ("id"));`);
|
|
13
|
+
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_faire_webhook_event_webhook_id_unique" ON "faire_webhook_event" ("webhook_id") WHERE deleted_at IS NULL;`);
|
|
14
|
+
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_faire_webhook_event_deleted_at" ON "faire_webhook_event" ("deleted_at") WHERE deleted_at IS NULL;`);
|
|
15
|
+
|
|
16
|
+
this.addSql(`alter table if exists "faire_sync_settings" drop column if exists "last_order_sync_at";`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from "@medusajs/framework/mikro-orm/migrations";
|
|
2
|
+
|
|
3
|
+
export class Migration20260709161201 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`alter table if exists "faire_sync_account" add column if not exists "auth_mode" text check ("auth_mode" in ('oauth', 'apiKey')) not null default 'oauth';`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
override async down(): Promise<void> {
|
|
10
|
+
this.addSql(`alter table if exists "faire_sync_account" drop column if exists "auth_mode";`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
}
|
|
@@ -6,6 +6,10 @@ const FaireSyncAccount = model.define("faire_sync_account", {
|
|
|
6
6
|
brand_name: model.text(),
|
|
7
7
|
currency: model.text().nullable(),
|
|
8
8
|
country: model.text().nullable(),
|
|
9
|
+
// How this account authenticates to Faire: OAuth (published apps) or a
|
|
10
|
+
// brand-issued API key (private/single-merchant integrations). Drives which
|
|
11
|
+
// auth header the client sends, so both connection types can coexist.
|
|
12
|
+
auth_mode: model.enum(["oauth", "apiKey"]).default("oauth"),
|
|
9
13
|
access_token: model.text(),
|
|
10
14
|
refresh_token: model.text().nullable(),
|
|
11
15
|
token_expires_at: model.dateTime().nullable(),
|
|
@@ -3,7 +3,6 @@ import FaireSyncAccount from "./models/faire-sync-account"
|
|
|
3
3
|
import FaireSyncRecord from "./models/faire-sync-record"
|
|
4
4
|
import FaireSyncSettings from "./models/faire-sync-settings"
|
|
5
5
|
import FaireSyncBatch from "./models/faire-sync-batch"
|
|
6
|
-
import FaireWebhookEvent from "./models/faire-webhook-event"
|
|
7
6
|
import FaireOrder from "./models/faire-order"
|
|
8
7
|
import { FaireClient } from "../../lib/faire-client"
|
|
9
8
|
import { encryptSecret, decryptSecret } from "../../lib/crypto"
|
|
@@ -83,7 +82,6 @@ class FaireSyncService extends MedusaService({
|
|
|
83
82
|
FaireSyncRecord,
|
|
84
83
|
FaireSyncSettings,
|
|
85
84
|
FaireSyncBatch,
|
|
86
|
-
FaireWebhookEvent,
|
|
87
85
|
FaireOrder,
|
|
88
86
|
}) {
|
|
89
87
|
protected options_: ModuleOptions
|
|
@@ -94,7 +92,15 @@ class FaireSyncService extends MedusaService({
|
|
|
94
92
|
this.options_ = resolveFaireOptions(options)
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Build a Faire client. Pass `authModeOverride` (e.g. an account's `auth_mode`)
|
|
97
|
+
* to force the auth header for that connection type; without it, the plugin's
|
|
98
|
+
* configured default is used (cached).
|
|
99
|
+
*/
|
|
100
|
+
getClient(authModeOverride?: "oauth" | "apiKey"): FaireClient {
|
|
101
|
+
if (authModeOverride) {
|
|
102
|
+
return new FaireClient({ ...this.options_, authMode: authModeOverride })
|
|
103
|
+
}
|
|
98
104
|
if (!this.client_) {
|
|
99
105
|
this.client_ = new FaireClient(this.options_)
|
|
100
106
|
}
|
|
@@ -130,7 +136,11 @@ class FaireSyncService extends MedusaService({
|
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
async saveAccount(
|
|
139
|
+
async saveAccount(
|
|
140
|
+
token: TokenData,
|
|
141
|
+
brand: BrandInfo,
|
|
142
|
+
authMode: "oauth" | "apiKey" = "oauth"
|
|
143
|
+
) {
|
|
134
144
|
const existing = await this.getActiveAccount()
|
|
135
145
|
const expiresAt =
|
|
136
146
|
token.expires_in != null
|
|
@@ -141,6 +151,7 @@ class FaireSyncService extends MedusaService({
|
|
|
141
151
|
brand_name: brand.brand_name,
|
|
142
152
|
currency: brand.currency ?? null,
|
|
143
153
|
country: brand.country ?? null,
|
|
154
|
+
auth_mode: authMode,
|
|
144
155
|
access_token: encryptSecret(token.access_token),
|
|
145
156
|
refresh_token: encryptSecret(token.refresh_token ?? null),
|
|
146
157
|
token_expires_at: expiresAt,
|
|
@@ -165,8 +176,9 @@ class FaireSyncService extends MedusaService({
|
|
|
165
176
|
// Best-effort: a failed revoke must not block local disconnect.
|
|
166
177
|
try {
|
|
167
178
|
const accessToken = decryptSecret((account as any).access_token)
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
// Only OAuth tokens are revocable; a brand-issued API key is not.
|
|
180
|
+
if (accessToken && (account as any).auth_mode !== "apiKey") {
|
|
181
|
+
await this.getClient("oauth").revokeToken(accessToken)
|
|
170
182
|
}
|
|
171
183
|
} catch (err: any) {
|
|
172
184
|
// eslint-disable-next-line no-console
|
|
@@ -256,12 +268,8 @@ class FaireSyncService extends MedusaService({
|
|
|
256
268
|
}
|
|
257
269
|
|
|
258
270
|
async startOAuth(): Promise<{ authorization_url: string; state: string }> {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
MedusaError.Types.INVALID_DATA,
|
|
262
|
-
"Faire is configured in API-key mode — use /admin/faire/auth/api-key to connect, not OAuth."
|
|
263
|
-
)
|
|
264
|
-
}
|
|
271
|
+
// Both connection types are available per-account, so OAuth is always
|
|
272
|
+
// offered here; the API-key path is a separate entry point.
|
|
265
273
|
try {
|
|
266
274
|
const state = crypto.randomUUID()
|
|
267
275
|
await this.updateSettings({
|
|
@@ -270,7 +278,7 @@ class FaireSyncService extends MedusaService({
|
|
|
270
278
|
created_at: Date.now(),
|
|
271
279
|
},
|
|
272
280
|
})
|
|
273
|
-
const client = this.getClient()
|
|
281
|
+
const client = this.getClient("oauth")
|
|
274
282
|
return {
|
|
275
283
|
authorization_url: client.getAuthorizationUrl(state),
|
|
276
284
|
state,
|
|
@@ -292,14 +300,16 @@ class FaireSyncService extends MedusaService({
|
|
|
292
300
|
)
|
|
293
301
|
}
|
|
294
302
|
try {
|
|
295
|
-
|
|
303
|
+
// Force API-key auth (X-FAIRE-ACCESS-TOKEN) for this connection regardless
|
|
304
|
+
// of the plugin's default mode, and persist it so later API calls use it.
|
|
305
|
+
const client = this.getClient("apiKey")
|
|
296
306
|
const brand = await client.getBrand(accessToken)
|
|
297
307
|
const token: TokenData = {
|
|
298
308
|
access_token: accessToken,
|
|
299
309
|
token_type: "Bearer",
|
|
300
310
|
retrieved_at: Date.now(),
|
|
301
311
|
}
|
|
302
|
-
const account = await this.saveAccount(token, brand)
|
|
312
|
+
const account = await this.saveAccount(token, brand, "apiKey")
|
|
303
313
|
await this.updateSettings({
|
|
304
314
|
account_id: account.id,
|
|
305
315
|
default_brand_id: brand.brand_id,
|
|
@@ -320,11 +330,12 @@ class FaireSyncService extends MedusaService({
|
|
|
320
330
|
"Invalid or expired OAuth state. Please start the Faire connection again."
|
|
321
331
|
)
|
|
322
332
|
}
|
|
333
|
+
let token: TokenData | undefined
|
|
323
334
|
try {
|
|
324
335
|
const client = this.getClient()
|
|
325
|
-
|
|
336
|
+
token = await client.exchangeCodeForToken(code)
|
|
326
337
|
const brand = await client.getBrand(token.access_token)
|
|
327
|
-
const account = await this.saveAccount(token, brand)
|
|
338
|
+
const account = await this.saveAccount(token, brand, "oauth")
|
|
328
339
|
await this.updateSettings({
|
|
329
340
|
account_id: account.id,
|
|
330
341
|
default_brand_id: brand.brand_id,
|
|
@@ -332,6 +343,17 @@ class FaireSyncService extends MedusaService({
|
|
|
332
343
|
})
|
|
333
344
|
return { account, brand }
|
|
334
345
|
} catch (err) {
|
|
346
|
+
// Self-heal a dropped OAuth so the next attempt starts clean:
|
|
347
|
+
// 1. clear the pending state (otherwise it sticks and confuses retries), and
|
|
348
|
+
// 2. if the code exchange succeeded but a later step failed, REVOKE the
|
|
349
|
+
// just-obtained token — otherwise Faire keeps the app "installed via an
|
|
350
|
+
// active OAuth access token" and every reconnect fails with that 400.
|
|
351
|
+
await this.updateSettings({ pending_oauth: null }).catch(() => {})
|
|
352
|
+
if (token?.access_token) {
|
|
353
|
+
await this.getClient()
|
|
354
|
+
.revokeToken(token.access_token)
|
|
355
|
+
.catch(() => {})
|
|
356
|
+
}
|
|
335
357
|
throw toMedusaError(err)
|
|
336
358
|
}
|
|
337
359
|
}
|
|
@@ -70,7 +70,7 @@ const processBatchStep = createStep(
|
|
|
70
70
|
): Promise<StepResponse<{ synced: number; failed: number }>> => {
|
|
71
71
|
const service: FaireSyncService = container.resolve(FAIRE_SYNC_MODULE)
|
|
72
72
|
const account = await service.ensureFreshToken()
|
|
73
|
-
const client = service.getClient()
|
|
73
|
+
const client = service.getClient((account as any).auth_mode)
|
|
74
74
|
|
|
75
75
|
// Resolve the incremental window. Precedence:
|
|
76
76
|
// explicit input > persisted high-water mark > full backfill (undefined).
|
|
@@ -26,6 +26,7 @@ type ResolvedConfig = {
|
|
|
26
26
|
account_id: string
|
|
27
27
|
brand_id: string
|
|
28
28
|
access_token: string
|
|
29
|
+
auth_mode: "oauth" | "apiKey"
|
|
29
30
|
currency: string | null
|
|
30
31
|
settings: any
|
|
31
32
|
}
|
|
@@ -66,6 +67,7 @@ const resolveFaireConfigStep = createStep(
|
|
|
66
67
|
account_id: account.id,
|
|
67
68
|
brand_id: account.brand_id,
|
|
68
69
|
access_token: account.access_token,
|
|
70
|
+
auth_mode: (account.auth_mode as "oauth" | "apiKey") ?? "oauth",
|
|
69
71
|
currency: account.currency ?? null,
|
|
70
72
|
settings,
|
|
71
73
|
})
|
|
@@ -211,7 +213,7 @@ const syncProductStep = createStep(
|
|
|
211
213
|
{ container }
|
|
212
214
|
): Promise<StepResponse<SyncResult>> => {
|
|
213
215
|
const service: FaireSyncService = container.resolve(FAIRE_SYNC_MODULE)
|
|
214
|
-
const client = service.getClient()
|
|
216
|
+
const client = service.getClient(input.config.auth_mode)
|
|
215
217
|
const { access_token, settings } = input.config
|
|
216
218
|
const { product_input, existing_product_token, product_status } =
|
|
217
219
|
input.prepared
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const utils_1 = require("@medusajs/framework/utils");
|
|
4
|
-
/**
|
|
5
|
-
* Durable record of an inbound Faire webhook delivery. `webhook_id` is the
|
|
6
|
-
* delivery id Faire sends (unique) so retries are idempotent.
|
|
7
|
-
*/
|
|
8
|
-
const FaireWebhookEvent = utils_1.model.define("faire_webhook_event", {
|
|
9
|
-
id: utils_1.model.id().primaryKey(),
|
|
10
|
-
webhook_id: utils_1.model.text().unique(),
|
|
11
|
-
event_type: utils_1.model.text(),
|
|
12
|
-
brand_id: utils_1.model.text().nullable(),
|
|
13
|
-
resource_url: utils_1.model.text().nullable(),
|
|
14
|
-
payload: utils_1.model.json().nullable(),
|
|
15
|
-
resource: utils_1.model.json().nullable(),
|
|
16
|
-
processed: utils_1.model.boolean().default(false),
|
|
17
|
-
error: utils_1.model.text().nullable(),
|
|
18
|
-
received_at: utils_1.model.dateTime().nullable(),
|
|
19
|
-
});
|
|
20
|
-
exports.default = FaireWebhookEvent;
|
|
21
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmFpcmUtd2ViaG9vay1ldmVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL2ZhaXJlLXN5bmMvbW9kZWxzL2ZhaXJlLXdlYmhvb2stZXZlbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxxREFBaUQ7QUFFakQ7OztHQUdHO0FBQ0gsTUFBTSxpQkFBaUIsR0FBRyxhQUFLLENBQUMsTUFBTSxDQUFDLHFCQUFxQixFQUFFO0lBQzVELEVBQUUsRUFBRSxhQUFLLENBQUMsRUFBRSxFQUFFLENBQUMsVUFBVSxFQUFFO0lBQzNCLFVBQVUsRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsTUFBTSxFQUFFO0lBQ2pDLFVBQVUsRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFO0lBQ3hCLFFBQVEsRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ2pDLFlBQVksRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ3JDLE9BQU8sRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ2hDLFFBQVEsRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsUUFBUSxFQUFFO0lBQ2pDLFNBQVMsRUFBRSxhQUFLLENBQUMsT0FBTyxFQUFFLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQztJQUN6QyxLQUFLLEVBQUUsYUFBSyxDQUFDLElBQUksRUFBRSxDQUFDLFFBQVEsRUFBRTtJQUM5QixXQUFXLEVBQUUsYUFBSyxDQUFDLFFBQVEsRUFBRSxDQUFDLFFBQVEsRUFBRTtDQUN6QyxDQUFDLENBQUE7QUFFRixrQkFBZSxpQkFBaUIsQ0FBQSJ9
|