@fitzzero/quickdraw-core 2.0.0 → 2.1.0
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/client/index.d.ts +97 -1
- package/dist/client/index.js +210 -1
- package/dist/client/index.js.map +1 -1
- package/dist/server/index.d.ts +160 -1
- package/dist/server/index.js +675 -1
- package/dist/server/index.js.map +1 -1
- package/eslint-plugin-quickdraw/index.mjs +14 -0
- package/eslint-plugin-quickdraw/rules/no-direct-prisma-mutations.mjs +49 -0
- package/eslint-plugin-quickdraw/rules/no-service-method-record.mjs +35 -0
- package/eslint-plugin-quickdraw/rules/no-unsafe-payload-cast.mjs +41 -0
- package/eslint-plugin-quickdraw/rules/require-zod-schema.mjs +60 -0
- package/package.json +6 -2
package/dist/client/index.d.ts
CHANGED
|
@@ -385,4 +385,100 @@ interface SocketSwitchProps<TEntry, TAllowedUpdate, TKey extends keyof TAllowedU
|
|
|
385
385
|
*/
|
|
386
386
|
declare function SocketSwitch<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate>(props: SocketSwitchProps<TEntry, TAllowedUpdate, TKey>): React.ReactElement;
|
|
387
387
|
|
|
388
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Format a currency value for display.
|
|
390
|
+
*/
|
|
391
|
+
declare function formatCurrency(value: string | number | null | undefined, currency?: string, locale?: string): string;
|
|
392
|
+
/**
|
|
393
|
+
* Format a number for display with optional decimal places.
|
|
394
|
+
*/
|
|
395
|
+
declare function formatNumber(value: string | number | null | undefined, options?: Intl.NumberFormatOptions, locale?: string): string;
|
|
396
|
+
/**
|
|
397
|
+
* Format a date for display.
|
|
398
|
+
*/
|
|
399
|
+
declare function formatDate(value: string | Date | null | undefined, options?: Intl.DateTimeFormatOptions, locale?: string): string;
|
|
400
|
+
/**
|
|
401
|
+
* Format a date with time for display.
|
|
402
|
+
*/
|
|
403
|
+
declare function formatDateTime(value: string | Date | null | undefined, options?: Intl.DateTimeFormatOptions, locale?: string): string;
|
|
404
|
+
/**
|
|
405
|
+
* Truncate a string to a maximum length.
|
|
406
|
+
*/
|
|
407
|
+
declare function truncate(value: string | null | undefined, maxLength?: number): string;
|
|
408
|
+
/**
|
|
409
|
+
* Format a percentage for display.
|
|
410
|
+
*/
|
|
411
|
+
declare function formatPercent(value: string | number | null | undefined, decimals?: number, locale?: string): string;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get the stored auth token from localStorage.
|
|
415
|
+
*/
|
|
416
|
+
declare function getAuthToken(): string | null;
|
|
417
|
+
/**
|
|
418
|
+
* Store the auth token in localStorage and dispatch a change event.
|
|
419
|
+
*/
|
|
420
|
+
declare function setAuthToken(token: string): void;
|
|
421
|
+
/**
|
|
422
|
+
* Remove the auth token from localStorage and dispatch a change event.
|
|
423
|
+
*/
|
|
424
|
+
declare function clearAuthToken(): void;
|
|
425
|
+
interface JWTPayload {
|
|
426
|
+
userId: string;
|
|
427
|
+
email?: string;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Parse JWT payload client-side (not verified, for display purposes only).
|
|
431
|
+
*/
|
|
432
|
+
declare function parseJWTPayload(token: string): JWTPayload | null;
|
|
433
|
+
/**
|
|
434
|
+
* Build an OAuth redirect URL for a given provider.
|
|
435
|
+
*/
|
|
436
|
+
declare function getOAuthUrl(provider: string, apiUrl?: string): string;
|
|
437
|
+
/**
|
|
438
|
+
* Logout from current session (invalidate token on server and clear localStorage).
|
|
439
|
+
*/
|
|
440
|
+
declare function logout(apiUrl?: string): Promise<void>;
|
|
441
|
+
/**
|
|
442
|
+
* Logout from all devices (invalidate all sessions and clear localStorage).
|
|
443
|
+
* Returns the number of sessions invalidated.
|
|
444
|
+
*/
|
|
445
|
+
declare function logoutAllDevices(apiUrl?: string): Promise<number>;
|
|
446
|
+
|
|
447
|
+
interface NavItem {
|
|
448
|
+
id: string;
|
|
449
|
+
label: string;
|
|
450
|
+
href: string;
|
|
451
|
+
icon?: unknown;
|
|
452
|
+
children?: NavItem[];
|
|
453
|
+
defaultExpanded?: boolean;
|
|
454
|
+
requireAuth?: boolean;
|
|
455
|
+
dynamicChildren?: boolean;
|
|
456
|
+
serviceName?: string;
|
|
457
|
+
minAccessLevel?: string;
|
|
458
|
+
}
|
|
459
|
+
interface BreadcrumbItem {
|
|
460
|
+
label: string;
|
|
461
|
+
href: string;
|
|
462
|
+
siblings?: {
|
|
463
|
+
label: string;
|
|
464
|
+
href: string;
|
|
465
|
+
}[];
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Find a nav item by its href (exact match, searching children recursively).
|
|
469
|
+
*/
|
|
470
|
+
declare function findNavItemByHref(items: NavItem[], href: string): NavItem | undefined;
|
|
471
|
+
/**
|
|
472
|
+
* Find the parent nav item containing a child with the given href.
|
|
473
|
+
*/
|
|
474
|
+
declare function findParentNavItem(items: NavItem[], href: string): NavItem | undefined;
|
|
475
|
+
/**
|
|
476
|
+
* Build breadcrumb items from a pathname using the provided navigation config.
|
|
477
|
+
*/
|
|
478
|
+
declare function buildBreadcrumbs(pathname: string, navigation: NavItem[]): BreadcrumbItem[];
|
|
479
|
+
/**
|
|
480
|
+
* Check if a route requires authentication by searching the navigation config.
|
|
481
|
+
*/
|
|
482
|
+
declare function routeRequiresAuth(pathname: string, navigation: NavItem[], userMenuItems?: NavItem[]): boolean;
|
|
483
|
+
|
|
484
|
+
export { type BreadcrumbItem, type CommitMode, type JWTPayload, type NavItem, QuickdrawProvider, QuickdrawProviderProps, QuickdrawSocketContextValue, SocketCheckbox, type SocketCheckboxProps, SocketSelect, type SocketSelectProps, SocketSlider, type SocketSliderProps, SocketSwitch, type SocketSwitchProps, SocketTextField, type SocketTextFieldProps, UseServiceOptions, UseServiceQueryOptions, UseServiceQueryResult, UseServiceResult, type UseSocketInputOptions, type UseSocketInputResult, UseSubscriptionOptions, UseSubscriptionResult, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useQuickdrawSocket, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
|
package/dist/client/index.js
CHANGED
|
@@ -849,6 +849,215 @@ function SocketSwitch(props) {
|
|
|
849
849
|
);
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
-
|
|
852
|
+
// src/client/utils/formatting.ts
|
|
853
|
+
function formatCurrency(value, currency = "USD", locale = "en-US") {
|
|
854
|
+
if (value == null) return "-";
|
|
855
|
+
const num = typeof value === "string" ? parseFloat(value) : value;
|
|
856
|
+
if (isNaN(num)) return "-";
|
|
857
|
+
return new Intl.NumberFormat(locale, {
|
|
858
|
+
style: "currency",
|
|
859
|
+
currency
|
|
860
|
+
}).format(num);
|
|
861
|
+
}
|
|
862
|
+
function formatNumber(value, options, locale = "en-US") {
|
|
863
|
+
if (value == null) return "-";
|
|
864
|
+
const num = typeof value === "string" ? parseFloat(value) : value;
|
|
865
|
+
if (isNaN(num)) return "-";
|
|
866
|
+
return new Intl.NumberFormat(locale, options).format(num);
|
|
867
|
+
}
|
|
868
|
+
function formatDate(value, options, locale = "en-US") {
|
|
869
|
+
if (!value) return "-";
|
|
870
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
871
|
+
if (isNaN(date.getTime())) return "-";
|
|
872
|
+
return date.toLocaleDateString(
|
|
873
|
+
locale,
|
|
874
|
+
options ?? {
|
|
875
|
+
year: "numeric",
|
|
876
|
+
month: "short",
|
|
877
|
+
day: "numeric"
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
function formatDateTime(value, options, locale = "en-US") {
|
|
882
|
+
if (!value) return "-";
|
|
883
|
+
const date = typeof value === "string" ? new Date(value) : value;
|
|
884
|
+
if (isNaN(date.getTime())) return "-";
|
|
885
|
+
return date.toLocaleString(
|
|
886
|
+
locale,
|
|
887
|
+
options ?? {
|
|
888
|
+
year: "numeric",
|
|
889
|
+
month: "short",
|
|
890
|
+
day: "numeric",
|
|
891
|
+
hour: "numeric",
|
|
892
|
+
minute: "2-digit"
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
function truncate(value, maxLength = 50) {
|
|
897
|
+
if (!value) return "-";
|
|
898
|
+
if (value.length <= maxLength) return value;
|
|
899
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
900
|
+
}
|
|
901
|
+
function formatPercent(value, decimals = 1, locale = "en-US") {
|
|
902
|
+
if (value == null) return "-";
|
|
903
|
+
const num = typeof value === "string" ? parseFloat(value) : value;
|
|
904
|
+
if (isNaN(num)) return "-";
|
|
905
|
+
return new Intl.NumberFormat(locale, {
|
|
906
|
+
style: "percent",
|
|
907
|
+
minimumFractionDigits: decimals,
|
|
908
|
+
maximumFractionDigits: decimals
|
|
909
|
+
}).format(num / 100);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/client/utils/auth.ts
|
|
913
|
+
var AUTH_TOKEN_KEY = "auth_token";
|
|
914
|
+
function getAuthToken() {
|
|
915
|
+
if (typeof window === "undefined") return null;
|
|
916
|
+
return localStorage.getItem(AUTH_TOKEN_KEY);
|
|
917
|
+
}
|
|
918
|
+
function setAuthToken(token) {
|
|
919
|
+
if (typeof window === "undefined") return;
|
|
920
|
+
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
|
921
|
+
window.dispatchEvent(new Event("auth-token-changed"));
|
|
922
|
+
}
|
|
923
|
+
function clearAuthToken() {
|
|
924
|
+
if (typeof window === "undefined") return;
|
|
925
|
+
localStorage.removeItem(AUTH_TOKEN_KEY);
|
|
926
|
+
window.dispatchEvent(new Event("auth-token-changed"));
|
|
927
|
+
}
|
|
928
|
+
function parseJWTPayload(token) {
|
|
929
|
+
try {
|
|
930
|
+
const parts = token.split(".");
|
|
931
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
932
|
+
const decoded = atob(parts[1]);
|
|
933
|
+
const payload = JSON.parse(decoded);
|
|
934
|
+
if (typeof payload === "object" && payload !== null && "userId" in payload && typeof payload.userId === "string") {
|
|
935
|
+
const typedPayload = payload;
|
|
936
|
+
return {
|
|
937
|
+
userId: typedPayload.userId,
|
|
938
|
+
email: typedPayload.email
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
return null;
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function getOAuthUrl(provider, apiUrl) {
|
|
947
|
+
const base = apiUrl ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
948
|
+
return `${base}/auth/${provider}`;
|
|
949
|
+
}
|
|
950
|
+
async function logout(apiUrl) {
|
|
951
|
+
const base = apiUrl ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
952
|
+
const token = getAuthToken();
|
|
953
|
+
if (token) {
|
|
954
|
+
try {
|
|
955
|
+
await fetch(`${base}/auth/logout`, {
|
|
956
|
+
method: "DELETE",
|
|
957
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
958
|
+
});
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
clearAuthToken();
|
|
963
|
+
}
|
|
964
|
+
async function logoutAllDevices(apiUrl) {
|
|
965
|
+
const base = apiUrl ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
966
|
+
const token = getAuthToken();
|
|
967
|
+
if (!token) {
|
|
968
|
+
clearAuthToken();
|
|
969
|
+
return 0;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const response = await fetch(`${base}/auth/sessions`, {
|
|
973
|
+
method: "DELETE",
|
|
974
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
975
|
+
});
|
|
976
|
+
if (response.ok) {
|
|
977
|
+
const data = await response.json();
|
|
978
|
+
clearAuthToken();
|
|
979
|
+
return data.sessionsDeleted ?? 0;
|
|
980
|
+
}
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
clearAuthToken();
|
|
984
|
+
return 0;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/client/utils/navigation.ts
|
|
988
|
+
function findNavItemByHref(items, href) {
|
|
989
|
+
for (const item of items) {
|
|
990
|
+
if (item.href === href) return item;
|
|
991
|
+
if (item.children) {
|
|
992
|
+
const found = findNavItemByHref(item.children, href);
|
|
993
|
+
if (found) return found;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return void 0;
|
|
997
|
+
}
|
|
998
|
+
function findParentNavItem(items, href) {
|
|
999
|
+
for (const item of items) {
|
|
1000
|
+
if (item.children) {
|
|
1001
|
+
const childMatch = item.children.find((c) => c.href === href);
|
|
1002
|
+
if (childMatch) return item;
|
|
1003
|
+
const nested = findParentNavItem(item.children, href);
|
|
1004
|
+
if (nested) return nested;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return void 0;
|
|
1008
|
+
}
|
|
1009
|
+
function buildBreadcrumbs(pathname, navigation) {
|
|
1010
|
+
const breadcrumbs = [];
|
|
1011
|
+
if (pathname === "/") {
|
|
1012
|
+
breadcrumbs.push({
|
|
1013
|
+
label: "Home",
|
|
1014
|
+
href: "/",
|
|
1015
|
+
siblings: navigation.filter((item) => item.href === "/" || !item.requireAuth).map((item) => ({ label: item.label, href: item.href }))
|
|
1016
|
+
});
|
|
1017
|
+
return breadcrumbs;
|
|
1018
|
+
}
|
|
1019
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
1020
|
+
let currentPath = "";
|
|
1021
|
+
for (const segment of segments) {
|
|
1022
|
+
currentPath += `/${segment}`;
|
|
1023
|
+
const navItem = findNavItemByHref(navigation, currentPath);
|
|
1024
|
+
if (navItem) {
|
|
1025
|
+
const parent = findParentNavItem(navigation, currentPath);
|
|
1026
|
+
const siblingItems = parent?.children ?? navigation;
|
|
1027
|
+
breadcrumbs.push({
|
|
1028
|
+
label: navItem.label,
|
|
1029
|
+
href: navItem.href,
|
|
1030
|
+
siblings: siblingItems.map((item) => ({
|
|
1031
|
+
label: item.label,
|
|
1032
|
+
href: item.href
|
|
1033
|
+
}))
|
|
1034
|
+
});
|
|
1035
|
+
} else {
|
|
1036
|
+
breadcrumbs.push({
|
|
1037
|
+
label: segment,
|
|
1038
|
+
href: currentPath
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return breadcrumbs;
|
|
1043
|
+
}
|
|
1044
|
+
function routeRequiresAuth(pathname, navigation, userMenuItems) {
|
|
1045
|
+
const navItem = findNavItemByHref(navigation, pathname);
|
|
1046
|
+
if (navItem) return navItem.requireAuth ?? false;
|
|
1047
|
+
if (userMenuItems) {
|
|
1048
|
+
const userItem = findNavItemByHref(userMenuItems, pathname);
|
|
1049
|
+
if (userItem) return userItem.requireAuth ?? false;
|
|
1050
|
+
}
|
|
1051
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
1052
|
+
let currentPath = "";
|
|
1053
|
+
for (const segment of segments) {
|
|
1054
|
+
currentPath += `/${segment}`;
|
|
1055
|
+
const item = findNavItemByHref(navigation, currentPath);
|
|
1056
|
+
if (item?.requireAuth) return true;
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export { QuickdrawProvider, SocketCheckbox, SocketSelect, SocketSlider, SocketSwitch, SocketTextField, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useQuickdrawSocket, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
|
|
853
1062
|
//# sourceMappingURL=index.js.map
|
|
854
1063
|
//# sourceMappingURL=index.js.map
|