@flowselections/floriday-authenticatie-module 1.0.16 → 1.0.18
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-lib/components/floriday/FlorydayConnectionPage.d.ts.map +1 -1
- package/dist-lib/components/floriday/FlorydayConnectionPage.js +2 -1
- package/dist-lib/components/settings/FloricodeSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/FloricodeSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/FloricodeSettingsCard.js +235 -0
- package/dist-lib/components/settings/FlorydaySettingsCard.js +1 -1
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +484 -3
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +484 -3
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +484 -3
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +492 -3
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/floricode-credentials.server.d.ts +21 -0
- package/dist-lib/lib/floricode-credentials.server.d.ts.map +1 -0
- package/dist-lib/lib/floricode-credentials.server.js +158 -0
- package/dist-lib/lib/floricode-product-features.d.ts.map +1 -1
- package/dist-lib/lib/floricode-product-features.js +19 -33
- package/dist-lib/lib/floricode-regulatory-features.d.ts.map +1 -1
- package/dist-lib/lib/floricode-regulatory-features.js +51 -41
- package/dist-lib/lib/floricode-sync.server.d.ts +35 -0
- package/dist-lib/lib/floricode-sync.server.d.ts.map +1 -0
- package/dist-lib/lib/floricode-sync.server.js +211 -0
- package/dist-lib/lib/floricode.functions.d.ts +78555 -0
- package/dist-lib/lib/floricode.functions.d.ts.map +1 -0
- package/dist-lib/lib/floricode.functions.js +178 -0
- package/dist-lib/lib/floriday-auction-letters.functions.d.ts +484 -3
- package/dist-lib/lib/floriday-auction-letters.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-auto-process.server.d.ts +34 -0
- package/dist-lib/lib/floriday-auto-process.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-auto-process.server.js +365 -0
- package/dist-lib/lib/floriday-fulfillment-lookup.server.d.ts +19 -0
- package/dist-lib/lib/floriday-fulfillment-lookup.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-fulfillment-lookup.server.js +130 -0
- package/dist-lib/lib/floriday-gateway-auth.d.ts +24 -0
- package/dist-lib/lib/floriday-gateway-auth.d.ts.map +1 -0
- package/dist-lib/lib/floriday-gateway-auth.js +127 -0
- package/dist-lib/lib/floriday-labels.functions.d.ts +484 -3
- package/dist-lib/lib/floriday-labels.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-labels.functions.js +3 -2
- package/dist-lib/lib/floriday-public.functions.d.ts +2904 -18
- package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-rate-limit.server.d.ts +21 -0
- package/dist-lib/lib/floriday-rate-limit.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday-rate-limit.server.js +75 -0
- package/dist-lib/lib/floriday-stickers.functions.d.ts +484 -3
- package/dist-lib/lib/floriday-stickers.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-sync.functions.d.ts +15972 -99
- package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-writes.functions.d.ts +2952 -42
- package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-writes.functions.js +29 -4
- package/dist-lib/lib/floriday.functions.d.ts +9054 -198
- package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday.functions.js +39 -4
- package/dist-lib/lib/planning-helpers.d.ts +30 -0
- package/dist-lib/lib/planning-helpers.d.ts.map +1 -0
- package/dist-lib/lib/planning-helpers.js +62 -0
- package/package.json +5 -3
- package/public/flowselections-assets/floriday-authenticatie/floricode-logo.png +0 -0
- package/public/flowselections-assets/floriday-authenticatie/floriday-logo.png +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface Credentials {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
source: "database" | "env";
|
|
5
|
+
}
|
|
6
|
+
export declare function encryptClientSecret(plaintext: string): Promise<string>;
|
|
7
|
+
export declare function decryptClientSecret(ciphertext: string): Promise<string>;
|
|
8
|
+
export declare function getFloricodeCredentials(): Promise<Credentials>;
|
|
9
|
+
export declare function getFloricodeAccessToken(scope: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Test a single scope to validate credentials work. Writes the result back to
|
|
12
|
+
* `floricode_connection_settings.last_token_test_*`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function testFloricodeCredentials(): Promise<{
|
|
15
|
+
ok: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
scope: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function invalidateFloricodeTokenCache(): void;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=floricode-credentials.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floricode-credentials.server.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-credentials.server.ts"],"names":[],"mappings":"AAgBA,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC;CAC5B;AA0CD,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5E;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB7E;AAiBD,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,WAAW,CAAC,CAWpE;AAyBD,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5E;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBxG;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Floricode credentials store (server-only)
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// Centrale plek voor Floricode OAuth-inloggegevens. Waarden komen uit de tabel
|
|
5
|
+
// `public.floricode_connection_settings` en worden versleuteld opgeslagen met
|
|
6
|
+
// AES-256-GCM, met de encryption key uit env-var `FLORICODE_SECRET_ENCRYPTION_KEY`.
|
|
7
|
+
// Fallback naar env-vars `FLORICODE_CLIENT_ID/SECRET` bestaat alleen voor
|
|
8
|
+
// migratieperiode zolang de UI nog niet ingevuld is.
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Web Crypto API — werkt in browser, Node 15+ en Deno. Geen Node "crypto" import,
|
|
11
|
+
// zodat dist-lib client-side gebundeld kan worden zonder Node builtins te resolven.
|
|
12
|
+
import { supabaseAdmin } from "../integrations/supabase/client.server";
|
|
13
|
+
const FLORICODE_TOKEN_URL = "https://api.floricode.com/oauth/token";
|
|
14
|
+
const tokenCache = new Map();
|
|
15
|
+
function getSubtle() {
|
|
16
|
+
const subtle = globalThis.crypto?.subtle;
|
|
17
|
+
if (!subtle)
|
|
18
|
+
throw new Error("Web Crypto (crypto.subtle) is niet beschikbaar in deze runtime.");
|
|
19
|
+
return subtle;
|
|
20
|
+
}
|
|
21
|
+
function toBase64(bytes) {
|
|
22
|
+
let bin = "";
|
|
23
|
+
for (let i = 0; i < bytes.length; i++)
|
|
24
|
+
bin += String.fromCharCode(bytes[i]);
|
|
25
|
+
// btoa is beschikbaar in browser, Node 16+ en Deno.
|
|
26
|
+
return globalThis.btoa(bin);
|
|
27
|
+
}
|
|
28
|
+
function fromBase64(b64) {
|
|
29
|
+
const bin = globalThis.atob(b64);
|
|
30
|
+
const out = new Uint8Array(bin.length);
|
|
31
|
+
for (let i = 0; i < bin.length; i++)
|
|
32
|
+
out[i] = bin.charCodeAt(i);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
async function getEncryptionKey() {
|
|
36
|
+
const raw = process.env.FLORICODE_SECRET_ENCRYPTION_KEY;
|
|
37
|
+
if (!raw) {
|
|
38
|
+
throw new Error("FLORICODE_SECRET_ENCRYPTION_KEY ontbreekt — kan Floricode-credentials niet ontsleutelen.");
|
|
39
|
+
}
|
|
40
|
+
const subtle = getSubtle();
|
|
41
|
+
// Normaliseer naar 32 bytes via SHA-256, ongeacht lengte/format van de bron.
|
|
42
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(raw));
|
|
43
|
+
return subtle.importKey("raw", digest, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
44
|
+
}
|
|
45
|
+
export async function encryptClientSecret(plaintext) {
|
|
46
|
+
const subtle = getSubtle();
|
|
47
|
+
const key = await getEncryptionKey();
|
|
48
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
49
|
+
const cipherBuf = await subtle.encrypt({ name: "AES-GCM", iv: iv }, key, new TextEncoder().encode(plaintext));
|
|
50
|
+
const cipherBytes = new Uint8Array(cipherBuf);
|
|
51
|
+
// AES-GCM in WebCrypto plakt de 16-byte auth tag aan het einde van de ciphertext.
|
|
52
|
+
const tag = cipherBytes.slice(cipherBytes.length - 16);
|
|
53
|
+
const enc = cipherBytes.slice(0, cipherBytes.length - 16);
|
|
54
|
+
return `v1.${toBase64(iv)}.${toBase64(tag)}.${toBase64(enc)}`;
|
|
55
|
+
}
|
|
56
|
+
export async function decryptClientSecret(ciphertext) {
|
|
57
|
+
if (!ciphertext.startsWith("v1.")) {
|
|
58
|
+
throw new Error("Onbekend ciphertext-formaat voor Floricode-credential.");
|
|
59
|
+
}
|
|
60
|
+
const [, ivB64, tagB64, dataB64] = ciphertext.split(".");
|
|
61
|
+
if (!ivB64 || !tagB64 || !dataB64)
|
|
62
|
+
throw new Error("Ongeldig ciphertext-formaat.");
|
|
63
|
+
const subtle = getSubtle();
|
|
64
|
+
const key = await getEncryptionKey();
|
|
65
|
+
const iv = fromBase64(ivB64);
|
|
66
|
+
const tag = fromBase64(tagB64);
|
|
67
|
+
const data = fromBase64(dataB64);
|
|
68
|
+
const combined = new Uint8Array(data.length + tag.length);
|
|
69
|
+
combined.set(data, 0);
|
|
70
|
+
combined.set(tag, data.length);
|
|
71
|
+
const plainBuf = await subtle.decrypt({ name: "AES-GCM", iv: iv }, key, combined);
|
|
72
|
+
return new TextDecoder().decode(plainBuf);
|
|
73
|
+
}
|
|
74
|
+
async function loadCredentialsFromDb() {
|
|
75
|
+
const { data, error } = await supabaseAdmin
|
|
76
|
+
.from("floricode_connection_settings")
|
|
77
|
+
.select("client_id, client_secret_ciphertext")
|
|
78
|
+
.eq("id", 1)
|
|
79
|
+
.maybeSingle();
|
|
80
|
+
if (error)
|
|
81
|
+
throw new Error(`Kon Floricode-instellingen niet laden: ${error.message}`);
|
|
82
|
+
if (!data?.client_id || !data?.client_secret_ciphertext)
|
|
83
|
+
return null;
|
|
84
|
+
return {
|
|
85
|
+
clientId: data.client_id,
|
|
86
|
+
clientSecret: await decryptClientSecret(data.client_secret_ciphertext),
|
|
87
|
+
source: "database",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export async function getFloricodeCredentials() {
|
|
91
|
+
const fromDb = await loadCredentialsFromDb();
|
|
92
|
+
if (fromDb)
|
|
93
|
+
return fromDb;
|
|
94
|
+
const clientId = process.env.FLORICODE_CLIENT_ID;
|
|
95
|
+
const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
|
|
96
|
+
if (clientId && clientSecret) {
|
|
97
|
+
return { clientId, clientSecret, source: "env" };
|
|
98
|
+
}
|
|
99
|
+
throw new Error("Floricode-credentials ontbreken. Vul ze in via de Floricode-instellingen in het beheerscherm.");
|
|
100
|
+
}
|
|
101
|
+
async function requestToken(scope, creds) {
|
|
102
|
+
const body = new URLSearchParams({
|
|
103
|
+
grant_type: "client_credentials",
|
|
104
|
+
client_id: creds.clientId,
|
|
105
|
+
client_secret: creds.clientSecret,
|
|
106
|
+
scope,
|
|
107
|
+
});
|
|
108
|
+
const res = await fetch(FLORICODE_TOKEN_URL, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
111
|
+
body,
|
|
112
|
+
});
|
|
113
|
+
const json = (await res.json().catch(() => ({})));
|
|
114
|
+
if (!res.ok || !json?.access_token) {
|
|
115
|
+
throw new Error(`Floricode token (${scope}) mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
|
|
116
|
+
}
|
|
117
|
+
return { token: String(json.access_token), expiresIn: Number(json.expires_in ?? 3600) };
|
|
118
|
+
}
|
|
119
|
+
export async function getFloricodeAccessToken(scope) {
|
|
120
|
+
const cached = tokenCache.get(scope);
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
// 60s safety margin
|
|
123
|
+
if (cached && cached.expiresAt - 60000 > now)
|
|
124
|
+
return cached.token;
|
|
125
|
+
const creds = await getFloricodeCredentials();
|
|
126
|
+
const { token, expiresIn } = await requestToken(scope, creds);
|
|
127
|
+
tokenCache.set(scope, { token, expiresAt: now + expiresIn * 1000 });
|
|
128
|
+
return token;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Test a single scope to validate credentials work. Writes the result back to
|
|
132
|
+
* `floricode_connection_settings.last_token_test_*`.
|
|
133
|
+
*/
|
|
134
|
+
export async function testFloricodeCredentials() {
|
|
135
|
+
const scope = "vbn_productfeature:read";
|
|
136
|
+
let ok = false;
|
|
137
|
+
let error;
|
|
138
|
+
try {
|
|
139
|
+
const creds = await getFloricodeCredentials();
|
|
140
|
+
await requestToken(scope, creds);
|
|
141
|
+
ok = true;
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
error = String(e?.message ?? e);
|
|
145
|
+
}
|
|
146
|
+
await supabaseAdmin
|
|
147
|
+
.from("floricode_connection_settings")
|
|
148
|
+
.update({
|
|
149
|
+
last_token_test_at: new Date().toISOString(),
|
|
150
|
+
last_token_test_ok: ok,
|
|
151
|
+
last_token_test_error: ok ? null : error ?? "onbekende fout",
|
|
152
|
+
})
|
|
153
|
+
.eq("id", 1);
|
|
154
|
+
return { ok, error, scope };
|
|
155
|
+
}
|
|
156
|
+
export function invalidateFloricodeTokenCache() {
|
|
157
|
+
tokenCache.clear();
|
|
158
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floricode-product-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-product-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAShE,QAAA,MAAM,yBAAyB,+BAA+B,CAAC;AAc/D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"floricode-product-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-product-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAShE,QAAA,MAAM,yBAAyB,+BAA+B,CAAC;AAc/D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAoQD,wBAAsB,mCAAmC,CACvD,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,mBAAmB,EAChC,gBAAgB,EAAE,MAAM,GAAG,MAAM,EACjC,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,OAAO,CAAC;IACT,EAAE,EAAE,IAAI,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACnC,YAAY,EAAE,OAAO,yBAAyB,CAAC;IAC/C,eAAe,EAAE,qBAAqB,EAAE,CAAC;CAC1C,CAAC,CAiFD;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;GAiF1D"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchFloricodeRegulatoryFeaturesForProduct, upsertRegulatoryFeatureRecords, } from "./floricode-regulatory-features";
|
|
2
|
-
|
|
2
|
+
import { getFloricodeAccessToken as getSharedFloricodeToken } from "./floricode-credentials.server";
|
|
3
3
|
const FLORICODE_PRODUCT_FEATURE_URL = "https://api.floricode.com/v2/VBN/ProductFeature";
|
|
4
4
|
const PRODUCT_FEATURE_SCOPE = "vbn_productfeature:read";
|
|
5
5
|
const PRODUCT_FEATURE_LIST_NAME = "vbn-product-characteristic";
|
|
@@ -20,29 +20,7 @@ function chunk(items, size) {
|
|
|
20
20
|
return out;
|
|
21
21
|
}
|
|
22
22
|
async function getFloricodeAccessToken() {
|
|
23
|
-
|
|
24
|
-
const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
|
|
25
|
-
if (!clientId || !clientSecret) {
|
|
26
|
-
throw new Error("Floricode credentials ontbreken (FLORICODE_CLIENT_ID/FLORICODE_CLIENT_SECRET)");
|
|
27
|
-
}
|
|
28
|
-
const body = new URLSearchParams({
|
|
29
|
-
grant_type: "client_credentials",
|
|
30
|
-
client_id: clientId,
|
|
31
|
-
client_secret: clientSecret,
|
|
32
|
-
scope: PRODUCT_FEATURE_SCOPE,
|
|
33
|
-
});
|
|
34
|
-
const res = await fetch(FLORICODE_TOKEN_URL, {
|
|
35
|
-
method: "POST",
|
|
36
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
37
|
-
body,
|
|
38
|
-
});
|
|
39
|
-
const json = (await res.json().catch(() => ({})));
|
|
40
|
-
if (!res.ok) {
|
|
41
|
-
throw new Error(`Floricode token-aanvraag mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
|
|
42
|
-
}
|
|
43
|
-
if (!json?.access_token)
|
|
44
|
-
throw new Error("Floricode token-response bevat geen access_token");
|
|
45
|
-
return String(json.access_token);
|
|
23
|
+
return getSharedFloricodeToken(PRODUCT_FEATURE_SCOPE);
|
|
46
24
|
}
|
|
47
25
|
async function fetchFloricodeProductFeaturesForProduct(productCode) {
|
|
48
26
|
const token = await getFloricodeAccessToken();
|
|
@@ -74,21 +52,29 @@ async function fetchFloricodeProductFeaturesPage(token, skip, top) {
|
|
|
74
52
|
return Array.isArray(json?.value) ? json.value : [];
|
|
75
53
|
}
|
|
76
54
|
async function upsertFloricodeProductFeatureRecords(supabase, items) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
55
|
+
const dedup = new Map();
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
if (item?.product_id == null || !item?.feature_type_id)
|
|
59
|
+
continue;
|
|
80
60
|
const productCode = String(item.product_id);
|
|
81
61
|
const featureCode = String(item.feature_type_id);
|
|
82
62
|
const valueCode = item.feature_value_code == null ? "" : String(item.feature_value_code);
|
|
83
|
-
|
|
63
|
+
const record_id = `${productCode}:${featureCode}:${valueCode}`;
|
|
64
|
+
// Later occurrences win: keep the newest change_date_time
|
|
65
|
+
const existing = dedup.get(record_id);
|
|
66
|
+
if (existing && existing.change_date_time && item.change_date_time && existing.change_date_time > item.change_date_time)
|
|
67
|
+
continue;
|
|
68
|
+
dedup.set(record_id, {
|
|
84
69
|
resource: PRODUCT_FEATURE_RESOURCE,
|
|
85
|
-
record_id
|
|
70
|
+
record_id,
|
|
86
71
|
data: item,
|
|
87
72
|
change_date_time: item.change_date_time ?? null,
|
|
88
|
-
last_synced_at:
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
73
|
+
last_synced_at: now,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const rows = Array.from(dedup.values());
|
|
77
|
+
for (const part of chunk(rows, 250)) {
|
|
92
78
|
const { error } = await supabase
|
|
93
79
|
.from("floricode_records")
|
|
94
80
|
.upsert(part, { onConflict: "resource,record_id" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floricode-regulatory-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-regulatory-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAY5D,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"floricode-regulatory-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-regulatory-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAY5D,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAYD,wBAAsB,0CAA0C,CAC9D,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAgB3B;AAiDD,wBAAsB,8BAA8B,CAClD,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAiBD,wBAAsB,4BAA4B,CAAC,QAAQ,EAAE,cAAc;;;;;;GA2B1E;AAED,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;GAoK5E"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { getFloricodeAccessToken } from "./floricode-credentials.server";
|
|
2
2
|
const REG_FEATURE_TYPE_URL = "https://api.floricode.com/v2/VBN/RegulatoryFeatureType";
|
|
3
3
|
const REGULATION_TYPE_URL = "https://api.floricode.com/v2/VBN/RegulationType";
|
|
4
4
|
const REG_FEATURE_TYPE_RESOURCE = "RegulatoryFeatureType";
|
|
@@ -12,27 +12,7 @@ function chunk(items, size) {
|
|
|
12
12
|
return out;
|
|
13
13
|
}
|
|
14
14
|
async function getFloricodeToken(scope) {
|
|
15
|
-
|
|
16
|
-
const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
|
|
17
|
-
if (!clientId || !clientSecret) {
|
|
18
|
-
throw new Error("Floricode credentials ontbreken (FLORICODE_CLIENT_ID/FLORICODE_CLIENT_SECRET)");
|
|
19
|
-
}
|
|
20
|
-
const body = new URLSearchParams({
|
|
21
|
-
grant_type: "client_credentials",
|
|
22
|
-
client_id: clientId,
|
|
23
|
-
client_secret: clientSecret,
|
|
24
|
-
scope,
|
|
25
|
-
});
|
|
26
|
-
const res = await fetch(FLORICODE_TOKEN_URL, {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
29
|
-
body,
|
|
30
|
-
});
|
|
31
|
-
const json = (await res.json().catch(() => ({})));
|
|
32
|
-
if (!res.ok || !json?.access_token) {
|
|
33
|
-
throw new Error(`Floricode token (${scope}) mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
|
|
34
|
-
}
|
|
35
|
-
return String(json.access_token);
|
|
15
|
+
return getFloricodeAccessToken(scope);
|
|
36
16
|
}
|
|
37
17
|
export async function fetchFloricodeRegulatoryFeaturesForProduct(productCode) {
|
|
38
18
|
const token = await getFloricodeToken(REG_FEATURE_TYPE_SCOPE);
|
|
@@ -47,11 +27,18 @@ export async function fetchFloricodeRegulatoryFeaturesForProduct(productCode) {
|
|
|
47
27
|
}
|
|
48
28
|
return Array.isArray(json?.value) ? json.value : [];
|
|
49
29
|
}
|
|
50
|
-
async function fetchRegFeatureTypePage(token, skip, top) {
|
|
30
|
+
async function fetchRegFeatureTypePage(token, skip, top, sinceIso) {
|
|
51
31
|
const url = new URL(REG_FEATURE_TYPE_URL);
|
|
52
32
|
url.searchParams.set("$top", String(top));
|
|
53
33
|
url.searchParams.set("$skip", String(skip));
|
|
54
|
-
|
|
34
|
+
// Strict-unique tuple so OData $skip pagination is deterministic.
|
|
35
|
+
// Without regulation_type_id in the sort key, rows with multiple
|
|
36
|
+
// regulation_type_ids per (product, feature) can be silently skipped
|
|
37
|
+
// between pages — that's how S96/S97/S98/S99 fell out of the cache.
|
|
38
|
+
url.searchParams.set("$orderby", ["change_date_time", "product_id", "feature_type_id", "regulation_type_id"].join(","));
|
|
39
|
+
if (sinceIso) {
|
|
40
|
+
url.searchParams.set("$filter", `change_date_time gt ${sinceIso}`);
|
|
41
|
+
}
|
|
55
42
|
const res = await fetch(url.toString(), { headers: { authorization: `Bearer ${token}` } });
|
|
56
43
|
const json = (await res.json().catch(() => ({})));
|
|
57
44
|
if (!res.ok) {
|
|
@@ -84,7 +71,7 @@ export async function upsertRegulatoryFeatureRecords(supabase, items) {
|
|
|
84
71
|
});
|
|
85
72
|
}
|
|
86
73
|
const rows = Array.from(dedup.values());
|
|
87
|
-
for (const part of chunk(rows,
|
|
74
|
+
for (const part of chunk(rows, 250)) {
|
|
88
75
|
const { error } = await supabase
|
|
89
76
|
.from("floricode_records")
|
|
90
77
|
.upsert(part, { onConflict: "resource,record_id" });
|
|
@@ -136,7 +123,7 @@ export async function syncFloricodeRegulationTypes(supabase) {
|
|
|
136
123
|
const items = await fetchRegulationTypeAll(token);
|
|
137
124
|
const upserted = await upsertRegulationTypeRecords(supabase, items);
|
|
138
125
|
await setSyncState(supabase, REGULATION_TYPE_RESOURCE, {
|
|
139
|
-
status: "
|
|
126
|
+
status: "ok",
|
|
140
127
|
error_message: null,
|
|
141
128
|
last_sequence_number: upserted,
|
|
142
129
|
last_sync_at: new Date().toISOString(),
|
|
@@ -161,18 +148,31 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
|
|
|
161
148
|
// Read cursor from state so we can resume where the previous invocation stopped.
|
|
162
149
|
let skip = 0;
|
|
163
150
|
let maxChangeDateTime = null;
|
|
151
|
+
// sinceIso: watermark applied via $filter on subsequent runs after a full scan.
|
|
152
|
+
// - status 'running': we're mid full-scan or mid incremental-scan; keep the
|
|
153
|
+
// cursor/watermark that pass started with (do not filter differently mid-run).
|
|
154
|
+
// - status 'ok' with a watermark: kick off an incremental scan of only newer rows.
|
|
155
|
+
// - anything else (no state / error / no watermark yet): full re-scan from 0.
|
|
156
|
+
let sinceIso = null;
|
|
164
157
|
if (resume) {
|
|
165
158
|
const { data: state } = await supabase
|
|
166
159
|
.from("floricode_sync_state")
|
|
167
160
|
.select("cursor_skip,last_change_date_time,status")
|
|
168
161
|
.eq("resource", REG_FEATURE_TYPE_RESOURCE)
|
|
169
162
|
.maybeSingle();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
const s = state;
|
|
164
|
+
if (s?.status === "running") {
|
|
165
|
+
skip = Number(s.cursor_skip ?? 0) || 0;
|
|
166
|
+
maxChangeDateTime = s.last_change_date_time ?? null;
|
|
167
|
+
sinceIso = s.last_change_date_time ?? null;
|
|
168
|
+
}
|
|
169
|
+
else if (s?.status === "ok" && s.last_change_date_time) {
|
|
170
|
+
sinceIso = s.last_change_date_time;
|
|
171
|
+
maxChangeDateTime = s.last_change_date_time;
|
|
173
172
|
}
|
|
174
173
|
}
|
|
175
174
|
const startSkip = skip;
|
|
175
|
+
const incremental = Boolean(sinceIso);
|
|
176
176
|
let fetched = 0;
|
|
177
177
|
let upserted = 0;
|
|
178
178
|
let batches = 0;
|
|
@@ -181,12 +181,13 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
|
|
|
181
181
|
status: "running",
|
|
182
182
|
error_message: null,
|
|
183
183
|
cursor_skip: skip,
|
|
184
|
+
last_change_date_time: maxChangeDateTime,
|
|
184
185
|
last_sync_at: new Date().toISOString(),
|
|
185
186
|
});
|
|
186
187
|
try {
|
|
187
188
|
const token = await getFloricodeToken(REG_FEATURE_TYPE_SCOPE);
|
|
188
189
|
while (batches < maxBatches) {
|
|
189
|
-
const items = await fetchRegFeatureTypePage(token, skip, batchSize);
|
|
190
|
+
const items = await fetchRegFeatureTypePage(token, skip, batchSize, sinceIso);
|
|
190
191
|
if (items.length === 0) {
|
|
191
192
|
completed = true;
|
|
192
193
|
break;
|
|
@@ -208,19 +209,28 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
|
|
|
208
209
|
last_change_date_time: maxChangeDateTime,
|
|
209
210
|
last_sync_at: new Date().toISOString(),
|
|
210
211
|
});
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Ook bij partial runs projectie draaien zodat vbn-product-characteristic
|
|
217
|
-
// meegroeit met de raw cache. Fouten hier mogen de cursor niet blokkeren.
|
|
218
|
-
if (!completed && upserted > 0) {
|
|
212
|
+
// Lichte per-product projectie: alleen de zojuist geraakte producten
|
|
213
|
+
// opnieuw projecteren i.p.v. de volledige (200k+) rebuild na elke batch.
|
|
214
|
+
// Voorkomt "canceling statement due to statement timeout" tijdens partial syncs.
|
|
219
215
|
try {
|
|
220
|
-
|
|
216
|
+
const productIds = Array.from(new Set(items
|
|
217
|
+
.map((i) => (i?.product_id != null ? String(i.product_id) : null))
|
|
218
|
+
.filter((v) => Boolean(v))));
|
|
219
|
+
if (productIds.length > 0) {
|
|
220
|
+
// Chunk om payload-limieten en planner-blowups te vermijden.
|
|
221
|
+
for (const part of chunk(productIds, 500)) {
|
|
222
|
+
await supabase.rpc("refresh_floricode_derived_for_products", {
|
|
223
|
+
_product_ids: part,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
221
227
|
}
|
|
222
228
|
catch (e) {
|
|
223
|
-
console.warn("[regulatory] partial projection skipped:", String(e?.message ?? e));
|
|
229
|
+
console.warn("[regulatory] partial per-product projection skipped:", String(e?.message ?? e));
|
|
230
|
+
}
|
|
231
|
+
if (items.length < batchSize) {
|
|
232
|
+
completed = true;
|
|
233
|
+
break;
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
if (completed) {
|
|
@@ -235,7 +245,7 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
|
|
|
235
245
|
if (projectionError)
|
|
236
246
|
throw new Error(`Projectie naar code-lists mislukt: ${projectionError.message}`);
|
|
237
247
|
await setSyncState(supabase, REG_FEATURE_TYPE_RESOURCE, {
|
|
238
|
-
status: "
|
|
248
|
+
status: "ok",
|
|
239
249
|
error_message: null,
|
|
240
250
|
cursor_skip: 0,
|
|
241
251
|
last_sequence_number: skip,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface FloricodeResourceDef {
|
|
2
|
+
resource: string;
|
|
3
|
+
endpoint: string;
|
|
4
|
+
scope: string;
|
|
5
|
+
/** Columns forming the primary key; order used for $orderby unique sort. */
|
|
6
|
+
keyColumns: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare const FLORICODE_RESOURCES: FloricodeResourceDef[];
|
|
9
|
+
export interface FloricodeSyncResult {
|
|
10
|
+
ok: true;
|
|
11
|
+
resource: string;
|
|
12
|
+
fetched: number;
|
|
13
|
+
upserted: number;
|
|
14
|
+
batches: number;
|
|
15
|
+
finished: boolean;
|
|
16
|
+
nextWatermark: string | null;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function syncFloricodeResource(resource: string, opts?: {
|
|
20
|
+
batchSize?: number;
|
|
21
|
+
maxBatches?: number;
|
|
22
|
+
resume?: boolean;
|
|
23
|
+
}): Promise<FloricodeSyncResult>;
|
|
24
|
+
export declare function syncAllFloricodeResources(opts: {
|
|
25
|
+
batchSize?: number;
|
|
26
|
+
maxBatchesPerResource?: number;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
ok: true;
|
|
29
|
+
results: Array<FloricodeSyncResult | {
|
|
30
|
+
resource: string;
|
|
31
|
+
ok: false;
|
|
32
|
+
error: string;
|
|
33
|
+
}>;
|
|
34
|
+
}>;
|
|
35
|
+
//# sourceMappingURL=floricode-sync.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floricode-sync.server.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-sync.server.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,mBAAmB,EAAE,oBAAoB,EAkBrD,CAAC;AA6EF,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACvE,OAAO,CAAC,mBAAmB,CAAC,CAmH9B;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC,mBAAmB,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CAc9G"}
|