@svadmin/core 0.50.0 → 0.52.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.
Files changed (97) hide show
  1. package/package.json +21 -4
  2. package/src/access-control-contract.ts +250 -0
  3. package/src/adapters/casbin.ts +20 -14
  4. package/src/adapters/casl.ts +22 -19
  5. package/src/admin-tool.ts +7 -7
  6. package/src/audit-contract.ts +105 -0
  7. package/src/audit.ts +88 -128
  8. package/src/auth-hooks.svelte.ts +593 -261
  9. package/src/auth-mutation-contract.ts +143 -0
  10. package/src/auth-query-contract.ts +97 -0
  11. package/src/captured-mutation.svelte.ts +45 -0
  12. package/src/chatProvider.svelte.ts +1 -1
  13. package/src/command-contract.ts +195 -0
  14. package/src/context.svelte.ts +217 -205
  15. package/src/contract-context.svelte.ts +37 -0
  16. package/src/contract-form.svelte.ts +566 -0
  17. package/src/create-contract.ts +56 -0
  18. package/src/create-hooks.svelte.ts +347 -0
  19. package/src/create-many-contract.ts +90 -0
  20. package/src/create-many-hooks.svelte.ts +396 -0
  21. package/src/data-transfer.svelte.ts +1 -215
  22. package/src/defined-options.ts +50 -0
  23. package/src/delete-contract.ts +50 -0
  24. package/src/delete-hooks.svelte.ts +403 -0
  25. package/src/delete-many-contract.ts +60 -0
  26. package/src/delete-many-hooks.svelte.ts +402 -0
  27. package/src/enterprise.ts +4 -4
  28. package/src/export-format.ts +16 -14
  29. package/src/export-hooks.svelte.ts +191 -0
  30. package/src/feature-gate-contract.ts +53 -0
  31. package/src/hook-utils.svelte.ts +53 -105
  32. package/src/hooks.svelte.ts +19 -776
  33. package/src/http.ts +88 -87
  34. package/src/i18n.svelte.ts +56 -2
  35. package/src/import-contract.ts +106 -0
  36. package/src/import-hooks.svelte.ts +310 -0
  37. package/src/index.ts +77 -30
  38. package/src/inferencer-graphql.ts +7 -6
  39. package/src/infinite-query.svelte.ts +228 -0
  40. package/src/invalidation-contract.ts +26 -0
  41. package/src/live-sse.ts +91 -127
  42. package/src/live-subscription.svelte.ts +130 -0
  43. package/src/live-transport.ts +127 -0
  44. package/src/live-websocket.ts +85 -118
  45. package/src/live.svelte.ts +66 -68
  46. package/src/notification.svelte.ts +17 -15
  47. package/src/permission-catalog.ts +121 -0
  48. package/src/permission-hints-contract.ts +48 -0
  49. package/src/permissions.svelte.ts +112 -59
  50. package/src/plain-data.ts +61 -0
  51. package/src/provider-bundle.ts +85 -79
  52. package/src/provider-response.ts +35 -0
  53. package/src/query-hooks.svelte.ts +241 -288
  54. package/src/query-invalidation.ts +172 -0
  55. package/src/query-keys.ts +221 -110
  56. package/src/query-session.svelte.ts +216 -0
  57. package/src/query-snapshot.ts +144 -0
  58. package/src/reactive-projection.ts +53 -0
  59. package/src/record-decoder.ts +63 -0
  60. package/src/resource-binding.svelte.ts +19 -0
  61. package/src/resource-contract.ts +595 -0
  62. package/src/resource-mutation-types.ts +30 -0
  63. package/src/resource-schemas.ts +371 -0
  64. package/src/resource-transport-config.ts +156 -0
  65. package/src/route-parsing.ts +11 -11
  66. package/src/router-provider.ts +64 -59
  67. package/src/router.ts +8 -3
  68. package/src/routing-hooks.svelte.ts +90 -62
  69. package/src/schema-validation.ts +92 -0
  70. package/src/schema.ts +13 -0
  71. package/src/select-options.ts +95 -0
  72. package/src/select-query.svelte.ts +191 -0
  73. package/src/session-query.svelte.ts +88 -0
  74. package/src/strict-command-hooks.svelte.ts +333 -0
  75. package/src/strict-hooks.svelte.ts +198 -0
  76. package/src/table-hooks.svelte.ts +180 -174
  77. package/src/table-state.svelte.ts +117 -117
  78. package/src/task-contract.ts +87 -0
  79. package/src/task-hooks.svelte.ts +193 -149
  80. package/src/task-provider.ts +115 -0
  81. package/src/task-subscription.ts +132 -0
  82. package/src/theme.svelte.ts +155 -154
  83. package/src/toast.svelte.ts +38 -37
  84. package/src/types.ts +55 -108
  85. package/src/unsafe.ts +7 -0
  86. package/src/update-contract.ts +65 -0
  87. package/src/update-hooks.svelte.ts +347 -0
  88. package/src/update-many-contract.ts +79 -0
  89. package/src/update-many-hooks.svelte.ts +373 -0
  90. package/src/url-sync.ts +11 -10
  91. package/src/useCan.ts +96 -31
  92. package/src/useParsed.svelte.ts +18 -14
  93. package/src/useStepsForm.svelte.ts +123 -63
  94. package/src/utility-hooks.svelte.ts +101 -158
  95. package/src/form-hooks.svelte.ts +0 -889
  96. package/src/mutation-hooks.svelte.ts +0 -561
  97. package/src/schema-form.ts +0 -31
package/src/http.ts CHANGED
@@ -1,85 +1,82 @@
1
1
  /**
2
- * HTTP fetch 工具 — 带 CSRF 防护、401 自动跳转和结构化错误语义。
2
+ * HTTP fetch utilities with CSRF protection, automatic 401 redirects, and structured error semantics.
3
3
  */
4
4
 
5
- import { HttpError, type ValidationErrors } from './types';
5
+ import { HttpError,type ValidationErrors } from './types';
6
+ import { Type } from '@sinclair/typebox';
7
+ import { Value } from '@sinclair/typebox/value';
8
+ import { definedOptions } from './defined-options';
6
9
 
7
10
  export interface FetchWithInterceptorOptions {
8
- /** 401 时跳转的登录页路径 */
11
+ /** Login path used for 401 redirects. */
9
12
  loginPath?: string;
10
- /** 403 且服务端未返回消息时使用的错误消息 */
13
+ /** Error message used when a 403 response has no server-provided message. */
11
14
  forbiddenMessage?: string;
12
- /** 是否自动添加 X-Requested-With header (CSRF 防护) */
15
+ /** Whether to add the X-Requested-With header automatically for CSRF protection. */
13
16
  csrfProtection?: boolean;
14
- /** 自定义 fetch 实现,便于测试或 SSR 环境注入 */
17
+ /** Custom fetch implementation for testing or SSR injection. */
15
18
  fetchImpl?: FetchWithInterceptor;
16
- /** 自定义 401 重定向处理,便于测试或 SSR 环境注入 */
17
- onUnauthorized?: (loginPath: string, returnTo: string) => void;
19
+ /** Custom 401 redirect handler for testing or SSR injection. */
20
+ onUnauthorized?: (loginPath: string,returnTo: string) => void;
18
21
  }
19
22
 
20
- export type FetchWithInterceptor = (url: string, init?: RequestInit) => Promise<Response>;
23
+ export type FetchWithInterceptor=(url: string,init?: RequestInit) => Promise<Response>;
21
24
 
22
- const DEFAULT_OPTIONS: Required<Omit<FetchWithInterceptorOptions, 'fetchImpl' | 'onUnauthorized'>> & {
25
+ const DEFAULT_OPTIONS: Required<Omit<FetchWithInterceptorOptions,'fetchImpl'|'onUnauthorized'>>&{
23
26
  fetchImpl: FetchWithInterceptor;
24
- onUnauthorized: (loginPath: string, returnTo: string) => void;
25
- } = {
27
+ onUnauthorized: (loginPath: string,returnTo: string) => void;
28
+ }={
26
29
  loginPath: '/auth/login',
27
30
  forbiddenMessage: 'Forbidden: 该操作已被安全策略拒绝',
28
31
  csrfProtection: true,
29
32
  get fetchImpl() {
30
- if (typeof globalThis !== 'undefined' && typeof globalThis.fetch === 'function') {
31
- return (url: string, init?: RequestInit) => globalThis.fetch(url, init);
33
+ if(typeof globalThis!=='undefined'&&typeof globalThis.fetch==='function') {
34
+ return (url: string,init?: RequestInit) => globalThis.fetch(url,init);
32
35
  }
33
36
  return async () => {
34
37
  throw new Error('No fetch implementation found. Pass fetchImpl in options.');
35
38
  };
36
39
  },
37
- onUnauthorized: (loginPath, returnTo) => {
38
- if (typeof window !== 'undefined') {
39
- window.location.href = `${loginPath}?returnTo=${returnTo}`;
40
+ onUnauthorized: (loginPath,returnTo) => {
41
+ if(typeof window!=='undefined') {
42
+ window.location.href=`${loginPath}?returnTo=${returnTo}`;
40
43
  }
41
44
  },
42
45
  };
43
46
 
44
- function asRecord(value: unknown): Record<string, unknown> | null {
45
- return typeof value === 'object' && value !== null
46
- ? value as Record<string, unknown>
47
- : null;
47
+ function asRecord(value: unknown): Record<string,unknown>|null {
48
+ return Value.Check(Type.Record(Type.String(),Type.Unknown()),value)? value:null;
48
49
  }
49
50
 
50
- function asStructuredHttpError(error: unknown): HttpError | null {
51
- const record = asRecord(error);
52
- if (!record || typeof record.statusCode !== 'number') return null;
53
- const message = typeof record.message === 'string' ? record.message : 'HTTP request failed';
54
- const errors = asValidationErrors(record.errors);
55
- return new HttpError(message, record.statusCode, errors, {
56
- code: typeof record.code === 'string' ? record.code : undefined,
57
- details: record.details,
58
- body: record.body,
51
+ function asStructuredHttpError(error: unknown): HttpError|null {
52
+ const record=asRecord(error);
53
+ const status=record?.['statusCode'];
54
+ if(!record||typeof status!=='number'||!Number.isInteger(status)||status<100||status>599) return null;
55
+ const message=typeof record['message']==='string'? record['message']:'HTTP request failed';
56
+ const errors=asValidationErrors(record['errors']);
57
+ return new HttpError(message,status,errors,definedOptions({
58
+ code: typeof record['code']==='string'? record['code']:undefined,
59
+ details: record['details'],
60
+ body: record['body'],
59
61
  cause: error,
60
- });
62
+ }));
61
63
  }
62
64
 
63
- function firstString(...values: unknown[]): string | undefined {
64
- return values.find((value): value is string => typeof value === 'string');
65
+ function firstString(...values: unknown[]): string|undefined {
66
+ return values.find((value): value is string => typeof value==='string');
65
67
  }
66
68
 
67
- function asValidationErrors(value: unknown): ValidationErrors | undefined {
68
- const record = asRecord(value);
69
- if (!record) return undefined;
70
-
71
- const valid = Object.values(record).every((entry) => (
72
- typeof entry === 'string'
73
- || (Array.isArray(entry) && entry.every((item) => typeof item === 'string'))
74
- ));
75
- return valid ? record as ValidationErrors : undefined;
69
+ function asValidationErrors(value: unknown): ValidationErrors|undefined {
70
+ const schema=Type.Record(Type.String(),Type.Union([Type.String(),Type.Array(Type.String())]));
71
+ return Value.Check(schema,value)? value:undefined;
76
72
  }
77
73
 
78
74
  async function readErrorBody(response: Response): Promise<unknown> {
79
- const text = await response.text();
80
- if (!text) return undefined;
75
+ const text=await response.text();
76
+ if(!text) return undefined;
81
77
  try {
82
- return JSON.parse(text) as unknown;
78
+ const parsed: unknown=JSON.parse(text);
79
+ return parsed;
83
80
  } catch {
84
81
  return text;
85
82
  }
@@ -89,81 +86,85 @@ async function createResponseError(
89
86
  response: Response,
90
87
  forbiddenMessage: string,
91
88
  ): Promise<HttpError> {
92
- const body = await readErrorBody(response);
93
- const record = asRecord(body);
94
- const nestedError = asRecord(record?.error);
95
- const code = response.headers.get('X-Svadmin-Auth-Retry') === 'exhausted'
89
+ const body=await readErrorBody(response);
90
+ const record=asRecord(body);
91
+ const nestedError=asRecord(record?.['error']);
92
+ const code=response.headers.get('X-Svadmin-Auth-Retry')==='exhausted'
96
93
  ? 'auth_retry_exhausted'
97
- : firstString(
98
- record?.code,
99
- record?.error_code,
100
- nestedError?.code,
101
- nestedError?.error_code,
102
- record?.error,
94
+ :firstString(
95
+ record?.['code'],
96
+ record?.['error_code'],
97
+ nestedError?.['code'],
98
+ nestedError?.['error_code'],
99
+ record?.['error'],
103
100
  );
104
- const details = record?.details ?? nestedError?.details;
105
- const message = firstString(
106
- record?.message,
107
- nestedError?.message,
108
- record?.error_description,
109
- nestedError?.error_description,
101
+ const details=record?.['details']??nestedError?.['details'];
102
+ const message=firstString(
103
+ record?.['message'],
104
+ nestedError?.['message'],
105
+ record?.['error_description'],
106
+ nestedError?.['error_description'],
110
107
  body,
111
- ) ?? (response.status === 401 ? 'Unauthorized' : forbiddenMessage);
108
+ )??(response.status===401? 'Unauthorized':forbiddenMessage);
112
109
 
113
110
  return new HttpError(
114
111
  message,
115
112
  response.status,
116
- asValidationErrors(record?.errors ?? nestedError?.errors),
117
- { code, details, body },
113
+ asValidationErrors(record?.['errors']??nestedError?.['errors']),
114
+ definedOptions({
115
+ code,
116
+ details,
117
+ body,
118
+ }),
118
119
  );
119
120
  }
120
121
 
121
122
  /**
122
- * 带拦截器的 fetch。Core 只保留错误语义;认证刷新和请求重放由认证提供方处理。
123
+ * Fetch with interceptors. Core owns error semantics; the auth provider handles refresh and request replay.
123
124
  */
124
125
  export function createFetchWithInterceptor(
125
- options: FetchWithInterceptorOptions = {},
126
+ options: FetchWithInterceptorOptions={},
126
127
  ): FetchWithInterceptor {
127
- const opts = { ...DEFAULT_OPTIONS, ...options };
128
+ const opts={ ...DEFAULT_OPTIONS,...options };
128
129
 
129
- return async (url: string, init: RequestInit = {}): Promise<Response> => {
130
- const headers = new Headers(init.headers || {});
131
- if (
130
+ return async (url: string,init: RequestInit={}): Promise<Response> => {
131
+ const headers=new Headers(init.headers||{});
132
+ if(
132
133
  opts.csrfProtection
133
- && init.method
134
- && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(init.method.toUpperCase())
134
+ &&init.method
135
+ &&['POST','PUT','DELETE','PATCH'].includes(init.method.toUpperCase())
135
136
  ) {
136
- headers.set('X-Requested-With', 'XMLHttpRequest');
137
+ headers.set('X-Requested-With','XMLHttpRequest');
137
138
  }
138
139
 
139
140
  let response: Response;
140
141
  try {
141
- response = await opts.fetchImpl(url, { ...init, headers });
142
- } catch (error) {
143
- if (error instanceof HttpError) throw error;
144
- const structuredError = asStructuredHttpError(error);
145
- if (structuredError) throw structuredError;
142
+ response=await opts.fetchImpl(url,{ ...init,headers });
143
+ } catch(error) {
144
+ if(error instanceof HttpError) throw error;
145
+ const structuredError=asStructuredHttpError(error);
146
+ if(structuredError) throw structuredError;
146
147
  throw error;
147
148
  }
148
149
 
149
- if (response.ok || (response.status !== 401 && response.status !== 403)) {
150
+ if(response.ok||(response.status!==401&&response.status!==403)) {
150
151
  return response;
151
152
  }
152
153
 
153
- const httpError = await createResponseError(response, opts.forbiddenMessage);
154
- if (response.status === 401) {
155
- const returnTo = typeof window !== 'undefined'
154
+ const httpError=await createResponseError(response,opts.forbiddenMessage);
155
+ if(response.status===401) {
156
+ const returnTo=typeof window!=='undefined'
156
157
  ? encodeURIComponent(window.location.pathname)
157
- : '';
158
+ :'';
158
159
  try {
159
- opts.onUnauthorized(opts.loginPath, returnTo);
160
+ opts.onUnauthorized(opts.loginPath,returnTo);
160
161
  } catch {
161
- // 重定向适配器失败不能覆盖服务端的结构化 401 错误。
162
+ // Redirect adapter failures must not replace the server's structured 401 error.
162
163
  }
163
164
  }
164
165
  throw httpError;
165
166
  };
166
167
  }
167
168
 
168
- /** 便捷全局实例 — 使用默认配置。 */
169
- export const fetchWithInterceptor = createFetchWithInterceptor();
169
+ /** Convenient global instance using the default configuration. */
170
+ export const fetchWithInterceptor=createFetchWithInterceptor();
@@ -53,6 +53,31 @@ const baseLocales: Record<string, Record<string, string>> = {
53
53
  'common.page': '{current} / {total}',
54
54
  'common.export': '导出',
55
55
  'common.import': '导入',
56
+ 'common.chooseFile': '选择文件',
57
+ 'common.noFileChosen': '未选择文件',
58
+ 'common.filesSelected': '已选择 {count} 个文件',
59
+ 'common.uploadFile': '上传文件',
60
+ 'common.mapColumns': '映射列',
61
+ 'common.importProgress': '导入与校验',
62
+ 'common.dropFileHere': '将 CSV 或 JSON 文件拖放到这里',
63
+ 'common.fileFormatHint': '支持 UTF-8 编码的 .csv、.json 文件',
64
+ 'common.selectFile': '浏览文件',
65
+ 'common.detectedRows': '已检测',
66
+ 'common.records': '条记录',
67
+ 'common.ignoreColumn': '忽略',
68
+ 'common.startImport': '开始导入',
69
+ 'common.importing': '导入中...',
70
+ 'common.completed': '已完成',
71
+ 'common.succeeded': '成功',
72
+ 'common.failed': '失败',
73
+ 'common.failedRowsHint': '条记录导入失败。',
74
+ 'common.downloadErrors': '下载失败 CSV',
75
+ 'common.importAnother': '再次导入',
76
+ 'common.selectOption': '请选择',
77
+ 'common.yesTrue': '是 / 真',
78
+ 'common.noFalse': '否 / 假',
79
+ 'common.startDate': '开始日期',
80
+ 'common.endDate': '结束日期',
56
81
  'common.clone': '克隆',
57
82
  'common.autoSaving': '自动保存中...',
58
83
  'common.autoSaved': '已自动保存',
@@ -115,6 +140,8 @@ const baseLocales: Record<string, Record<string, string>> = {
115
140
  'common.saveView': '保存',
116
141
  'common.perPage': '每页',
117
142
  'common.copyId': '复制 ID',
143
+ 'common.moreActions': '更多操作',
144
+ 'common.quickEdit': '快速编辑',
118
145
  'common.showMore': '展开更多',
119
146
  'common.showLess': '收起',
120
147
  'common.showPassword': '显示密码',
@@ -724,6 +751,31 @@ const baseLocales: Record<string, Record<string, string>> = {
724
751
  'common.page': '{current} / {total}',
725
752
  'common.export': 'Export',
726
753
  'common.import': 'Import',
754
+ 'common.chooseFile': 'Choose file',
755
+ 'common.noFileChosen': 'No file chosen',
756
+ 'common.filesSelected': '{count} files selected',
757
+ 'common.uploadFile': 'Upload file',
758
+ 'common.mapColumns': 'Map columns',
759
+ 'common.importProgress': 'Import & verify',
760
+ 'common.dropFileHere': 'Drag and drop CSV or JSON file here',
761
+ 'common.fileFormatHint': 'Supports .csv, .json with UTF-8 encoding',
762
+ 'common.selectFile': 'Browse file',
763
+ 'common.detectedRows': 'Detected',
764
+ 'common.records': 'records',
765
+ 'common.ignoreColumn': 'Ignore',
766
+ 'common.startImport': 'Start import',
767
+ 'common.importing': 'Importing...',
768
+ 'common.completed': 'Completed',
769
+ 'common.succeeded': 'Succeeded',
770
+ 'common.failed': 'Failed',
771
+ 'common.failedRowsHint': 'records failed to import.',
772
+ 'common.downloadErrors': 'Download failed CSV',
773
+ 'common.importAnother': 'Import another',
774
+ 'common.selectOption': 'Select an option...',
775
+ 'common.yesTrue': 'Yes / True',
776
+ 'common.noFalse': 'No / False',
777
+ 'common.startDate': 'Start date',
778
+ 'common.endDate': 'End date',
727
779
  'common.clone': 'Clone',
728
780
  'common.autoSaving': 'Auto-saving...',
729
781
  'common.autoSaved': 'Saved',
@@ -786,6 +838,8 @@ const baseLocales: Record<string, Record<string, string>> = {
786
838
  'common.saveView': 'Save',
787
839
  'common.perPage': 'per page',
788
840
  'common.copyId': 'Copy ID',
841
+ 'common.moreActions': 'More actions',
842
+ 'common.quickEdit': 'Quick edit',
789
843
  'common.showMore': 'Show more',
790
844
  'common.showLess': 'Show less',
791
845
  'common.showPassword': 'Show password',
@@ -1355,7 +1409,7 @@ function detectLocale(): string {
1355
1409
  if (typeof navigator === 'undefined') return 'en';
1356
1410
  const browserLang = navigator.language || navigator.languages?.[0] || 'zh-CN';
1357
1411
  if (locales[browserLang]) return browserLang;
1358
- const prefix = browserLang.split('-')[0];
1412
+ const [prefix = browserLang] = browserLang.split('-');
1359
1413
  for (const key of Object.keys(locales)) {
1360
1414
  if (key === browserLang || key.startsWith(prefix + '-')) return key;
1361
1415
  }
@@ -1407,7 +1461,7 @@ function translateFromTables(
1407
1461
  params?: Record<string, string | number>,
1408
1462
  ): string {
1409
1463
  const localeTable = locales[locale];
1410
- let text = localeTable?.[key] ?? locales.en?.[key] ?? key;
1464
+ let text = localeTable?.[key] ?? locales['en']?.[key] ?? key;
1411
1465
 
1412
1466
  if (params) {
1413
1467
  for (const [param, value] of Object.entries(params)) {
@@ -0,0 +1,106 @@
1
+ import Papa from 'papaparse';
2
+ import { Type, type Static } from '@sinclair/typebox';
3
+ import { checkExact } from './schema-validation';
4
+ import { snapshotPlainData } from './plain-data';
5
+ import { HttpError } from './types';
6
+ import { parseContractId, parseContractRecord, type ContractSchemas, type ResourceContract } from './resource-contract';
7
+ import { decodeOneResult, decodeManyResult, rejectProviderResponse } from './record-decoder';
8
+
9
+ export type ImportFormat = 'auto' | 'csv' | 'json';
10
+ const progressSchema = Type.Object({
11
+ totalAmount: Type.Integer({ minimum: 0 }),
12
+ attemptedAmount: Type.Integer({ minimum: 0 }),
13
+ processedAmount: Type.Integer({ minimum: 0 }),
14
+ }, { additionalProperties: false });
15
+ export type ImportOperationProgress = Static<typeof progressSchema>;
16
+
17
+ export function newImportProgress(): ImportOperationProgress {
18
+ return { totalAmount: 0, attemptedAmount: 0, processedAmount: 0 };
19
+ }
20
+
21
+ export function snapshotImportProgress(value: unknown): ImportOperationProgress {
22
+ const progress = snapshotPlainData(value);
23
+ if (!checkExact(progressSchema, progress) || progress.processedAmount > progress.attemptedAmount ||
24
+ progress.attemptedAmount > progress.totalAmount ||
25
+ !Object.values(progress).every(Number.isSafeInteger)) throw new HttpError('Invalid import progress', 500);
26
+ return progress;
27
+ }
28
+
29
+ export function invalidImportInput(row?: number): never {
30
+ throw new HttpError('Invalid import input', 422, undefined, {
31
+ code: 'INVALID_RESOURCE_INPUT',
32
+ details: { phase: 'input', writeMayHaveSucceeded: false, ...(row === undefined ? {} : { row }) },
33
+ });
34
+ }
35
+
36
+ /** Files are the only non-JSON input; do not invoke caller-supplied getters or text methods. */
37
+ export function snapshotImportFile(value: unknown) {
38
+ try {
39
+ if (typeof value !== 'object' || value === null ||
40
+ ![Object.prototype, null].includes(Object.getPrototypeOf(value)) ||
41
+ Reflect.ownKeys(value).length !== 1) return invalidImportInput();
42
+ const property = Object.getOwnPropertyDescriptor(value, 'file');
43
+ const file: unknown = property && 'value' in property ? property.value : undefined;
44
+ if (!(file instanceof File)) return invalidImportInput();
45
+ const ownName = Object.getOwnPropertyDescriptor(file, 'name');
46
+ if (ownName && !('value' in ownName)) return invalidImportInput();
47
+ const name: unknown = ownName ? ownName.value : Object.getOwnPropertyDescriptor(File.prototype, 'name')?.get?.call(file);
48
+ if (typeof name !== 'string') return invalidImportInput();
49
+ const read = Blob.prototype.text.bind(file);
50
+ return { file, name, read };
51
+ } catch {
52
+ return invalidImportInput();
53
+ }
54
+ }
55
+
56
+ const rowsSchema = Type.Array(Type.Record(Type.String(), Type.Unknown()));
57
+ const csvSchema = Type.Array(Type.Array(Type.String()));
58
+
59
+ /** CSV values stay strings; only an explicit mapper may supply business conversions. */
60
+ export function parseImportRows(text: string, name: string, format: ImportFormat): Record<string, unknown>[] {
61
+ try {
62
+ const source = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
63
+ const selected = format === 'auto' ? name.toLowerCase().endsWith('.json') ? 'json'
64
+ : name.toLowerCase().endsWith('.csv') ? 'csv' : undefined : format;
65
+ if (selected === 'json') {
66
+ const data = snapshotPlainData(JSON.parse(source));
67
+ return checkExact(rowsSchema, data) ? data : invalidImportInput();
68
+ }
69
+ if (selected !== 'csv') return invalidImportInput();
70
+ const parsed = Papa.parse<unknown>(source, { delimiter: ',', dynamicTyping: false, skipEmptyLines: true });
71
+ const data = snapshotPlainData(parsed.data);
72
+ if (parsed.errors.length || !checkExact(csvSchema, data)) return invalidImportInput();
73
+ const [headers, ...rows] = data;
74
+ if (!headers) return [];
75
+ if (headers.some(header => header.trim() === '') || new Set(headers).size !== headers.length) return invalidImportInput();
76
+ return rows.map((row, index) => {
77
+ if (row.length !== headers.length) return invalidImportInput(index + 1);
78
+ return Object.fromEntries(headers.map((header, column) => [header, row[column]]));
79
+ });
80
+ } catch {
81
+ return invalidImportInput();
82
+ }
83
+ }
84
+
85
+ export function decodeImportReceipt<S extends ContractSchemas>(
86
+ contract: ResourceContract<S>, response: unknown, inputs: readonly unknown[],
87
+ batch: boolean, seen: ReadonlySet<string | number>,
88
+ ) {
89
+ try {
90
+ const receipt = snapshotPlainData(response);
91
+ const rows = batch
92
+ ? decodeManyResult(receipt, value => parseContractRecord(contract, value), true).data
93
+ : [decodeOneResult(receipt, value => parseContractRecord(contract, value), true).data];
94
+ if (rows.length !== inputs.length || new Set(rows.map(row => row.id)).size !== rows.length) rejectProviderResponse(true);
95
+ for (const [index, row] of rows.entries()) {
96
+ const recordId = parseContractId(contract, row.id);
97
+ const submitted = inputs[index];
98
+ const id: unknown = typeof submitted === 'object' && submitted !== null
99
+ ? Object.getOwnPropertyDescriptor(submitted, 'id')?.value : undefined;
100
+ if ((id !== undefined && id !== recordId) || seen.has(recordId)) rejectProviderResponse(true);
101
+ }
102
+ return rows;
103
+ } catch {
104
+ return rejectProviderResponse(true);
105
+ }
106
+ }