@igorchugurov/public-api-sdk 1.0.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/index.mjs ADDED
@@ -0,0 +1,1502 @@
1
+ import { createBrowserClient as createBrowserClient$1 } from '@supabase/ssr';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defProps = Object.defineProperties;
5
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
6
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
9
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
+ var __spreadValues = (a, b) => {
11
+ for (var prop in b || (b = {}))
12
+ if (__hasOwnProp.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ if (__getOwnPropSymbols)
15
+ for (var prop of __getOwnPropSymbols(b)) {
16
+ if (__propIsEnum.call(b, prop))
17
+ __defNormalProp(a, prop, b[prop]);
18
+ }
19
+ return a;
20
+ };
21
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
22
+
23
+ // src/types/entity-types.ts
24
+ function isFieldValue(value) {
25
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date || Array.isArray(value) && value.every((v) => typeof v === "string") || value === null || value === void 0;
26
+ }
27
+ function isEntityData(value) {
28
+ if (typeof value !== "object" || value === null) {
29
+ return false;
30
+ }
31
+ return Object.values(value).every(isFieldValue);
32
+ }
33
+ function getFieldValue(data, fieldName, defaultValue) {
34
+ const value = data[fieldName];
35
+ return value !== void 0 ? value : defaultValue;
36
+ }
37
+
38
+ // src/errors.ts
39
+ var SDKError = class extends Error {
40
+ constructor(code, message, statusCode, details) {
41
+ super(message);
42
+ this.code = code;
43
+ this.statusCode = statusCode;
44
+ this.details = details;
45
+ this.name = "SDKError";
46
+ }
47
+ };
48
+ var NotFoundError = class extends SDKError {
49
+ constructor(resource, id) {
50
+ super(
51
+ "NOT_FOUND",
52
+ id ? `${resource} with id ${id} not found` : `${resource} not found`,
53
+ 404
54
+ );
55
+ }
56
+ };
57
+ var PermissionDeniedError = class extends SDKError {
58
+ constructor(action, resource) {
59
+ super(
60
+ "PERMISSION_DENIED",
61
+ `Permission denied: cannot ${action} ${resource}`,
62
+ 403
63
+ );
64
+ }
65
+ };
66
+ var ValidationError = class extends SDKError {
67
+ constructor(field, message) {
68
+ super(
69
+ "VALIDATION_ERROR",
70
+ `Validation failed for ${field}: ${message}`,
71
+ 400,
72
+ {
73
+ field,
74
+ message
75
+ }
76
+ );
77
+ }
78
+ };
79
+ var AuthenticationError = class extends SDKError {
80
+ constructor(message = "Authentication required") {
81
+ super("AUTHENTICATION_REQUIRED", message, 401);
82
+ }
83
+ };
84
+ function handleSupabaseError(error) {
85
+ if (error.code === "PGRST116") {
86
+ throw new NotFoundError("Resource");
87
+ }
88
+ if (error.code === "23505") {
89
+ throw new SDKError("DUPLICATE_ENTRY", "Duplicate entry", 409);
90
+ }
91
+ if (error.code === "23503") {
92
+ throw new SDKError("FOREIGN_KEY_VIOLATION", "Foreign key violation", 400);
93
+ }
94
+ if (error.code === "42501") {
95
+ throw new PermissionDeniedError("access", "resource");
96
+ }
97
+ throw new SDKError(
98
+ "UNKNOWN_ERROR",
99
+ error.message || "Unknown error",
100
+ 500,
101
+ error
102
+ );
103
+ }
104
+ function handleInstanceError(error, instanceId) {
105
+ if ((error == null ? void 0 : error.code) === "PGRST116") {
106
+ throw new NotFoundError("Entity instance", instanceId);
107
+ }
108
+ if ((error == null ? void 0 : error.code) === "42501") {
109
+ throw new PermissionDeniedError("read", `entity instance ${instanceId}`);
110
+ }
111
+ handleSupabaseError(error);
112
+ }
113
+
114
+ // src/utils.ts
115
+ function validateInstanceData(data, fields) {
116
+ const errors = [];
117
+ for (const field of fields) {
118
+ const value = data[field.name];
119
+ if (field.required && (value === void 0 || value === null || value === "")) {
120
+ errors.push({
121
+ field: field.name,
122
+ message: `${field.name} is required`
123
+ });
124
+ continue;
125
+ }
126
+ if (value !== void 0 && value !== null) {
127
+ const typeError = validateFieldType(value, field);
128
+ if (typeError) {
129
+ errors.push({ field: field.name, message: typeError });
130
+ }
131
+ }
132
+ }
133
+ return {
134
+ valid: errors.length === 0,
135
+ errors
136
+ };
137
+ }
138
+ function validateFieldType(value, field) {
139
+ switch (field.dbType) {
140
+ case "varchar":
141
+ case "timestamptz":
142
+ if (typeof value !== "string") {
143
+ return `Expected string, got ${typeof value}`;
144
+ }
145
+ break;
146
+ case "float":
147
+ if (typeof value !== "number") {
148
+ return `Expected number, got ${typeof value}`;
149
+ }
150
+ break;
151
+ case "boolean":
152
+ if (typeof value !== "boolean") {
153
+ return `Expected boolean, got ${typeof value}`;
154
+ }
155
+ break;
156
+ // Relations - должны быть массивами строк (ID)
157
+ case "manyToMany":
158
+ case "oneToMany":
159
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string")) {
160
+ return `Expected array of strings (IDs), got ${typeof value}`;
161
+ }
162
+ break;
163
+ case "manyToOne":
164
+ case "oneToOne":
165
+ if (typeof value !== "string") {
166
+ return `Expected string (ID), got ${typeof value}`;
167
+ }
168
+ break;
169
+ }
170
+ return null;
171
+ }
172
+ function normalizeFieldValue(value, field) {
173
+ if (value === null || value === void 0) {
174
+ return null;
175
+ }
176
+ switch (field.dbType) {
177
+ case "float":
178
+ return typeof value === "string" ? parseFloat(value) : value;
179
+ case "boolean":
180
+ return typeof value === "string" ? value.toLowerCase() === "true" : Boolean(value);
181
+ case "varchar":
182
+ case "timestamptz":
183
+ return String(value);
184
+ default:
185
+ return value;
186
+ }
187
+ }
188
+ function toCamelCase(str) {
189
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
190
+ }
191
+ function toSnakeCase(str) {
192
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
193
+ }
194
+
195
+ // src/utils/generateUIConfig.ts
196
+ function generateUIConfig(entityDefinition, fields) {
197
+ const defaults = generateDefaults(entityDefinition, fields);
198
+ if (entityDefinition.uiConfig) {
199
+ return deepMerge(
200
+ defaults,
201
+ entityDefinition.uiConfig
202
+ );
203
+ }
204
+ return defaults;
205
+ }
206
+ function generateDefaults(entityDefinition, fields) {
207
+ var _a, _b, _c;
208
+ const name = entityDefinition.name;
209
+ const nameLower = name.toLowerCase();
210
+ const namePlural = pluralize(nameLower);
211
+ const tableName = entityDefinition.tableName;
212
+ const searchableFields = fields.filter((f) => f.searchable).map((f) => f.name);
213
+ const finalSearchableFields = searchableFields.length > 0 ? searchableFields : ["name"];
214
+ const list = {
215
+ pageTitle: name,
216
+ searchPlaceholder: `Search for ${namePlural}...`,
217
+ emptyStateTitle: `You have no ${namePlural}`,
218
+ emptyStateMessages: [
219
+ `${name}s that you create will end up here.`,
220
+ `Add a ${nameLower} to get started.`
221
+ ],
222
+ showCreateButton: true,
223
+ createButtonText: `New ${nameLower}`,
224
+ showSearch: true,
225
+ enablePagination: (_a = entityDefinition.enablePagination) != null ? _a : true,
226
+ pageSize: (_b = entityDefinition.pageSize) != null ? _b : 20,
227
+ enableFilters: (_c = entityDefinition.enableFilters) != null ? _c : false,
228
+ searchableFields: finalSearchableFields,
229
+ columns: generateColumns(fields)
230
+ };
231
+ const form = {
232
+ createPageTitle: `Create new ${nameLower}`,
233
+ editPageTitle: `Edit ${nameLower}`,
234
+ pageHeader: `${name} details`,
235
+ createButtonLabel: "Create",
236
+ updateButtonLabel: "Update",
237
+ cancelButtonLabel: "Cancel",
238
+ sectionTitles: {
239
+ 0: entityDefinition.titleSection0 || "General Information",
240
+ 1: entityDefinition.titleSection1 || "Section 1",
241
+ 2: entityDefinition.titleSection2 || "Section 2",
242
+ 3: entityDefinition.titleSection3 || "Section 3"
243
+ }
244
+ };
245
+ const messages = {
246
+ afterCreate: `${name} was created successfully!`,
247
+ afterUpdate: `${name} was updated successfully!`,
248
+ afterDelete: `${name} was deleted successfully!`,
249
+ errorCreate: `Failed to create ${nameLower}. Please try again.`,
250
+ errorUpdate: `Failed to update ${nameLower}. Please try again.`,
251
+ deleteModalTitle: `Confirm deleting ${nameLower}`,
252
+ deleteModalText: `Are you sure you want to delete "{itemName}"? This action cannot be undone.`,
253
+ deleteModalButtonText: "Delete",
254
+ reloadEvents: {
255
+ create: `reload${name}`,
256
+ update: `reload${name}`,
257
+ delete: `reload${name}`
258
+ }
259
+ };
260
+ return {
261
+ apiUrl: `/api/${tableName}`,
262
+ apiUrlAll: `/api/entity-instances/all`,
263
+ list,
264
+ form,
265
+ messages
266
+ };
267
+ }
268
+ function isRelationField(field) {
269
+ return field.dbType === "manyToOne" || field.dbType === "oneToOne" || field.dbType === "manyToMany" || field.dbType === "oneToMany";
270
+ }
271
+ function generateColumns(fields) {
272
+ const displayFields = fields.filter((f) => f.displayInTable).sort((a, b) => a.displayIndex - b.displayIndex);
273
+ const columns = displayFields.map((field, index) => {
274
+ const columnType = index === 0 ? "naigateToDetails" : getColumnType(field);
275
+ const isRelation = isRelationField(field);
276
+ const column = __spreadValues({
277
+ field: field.name,
278
+ headerName: field.label || field.name,
279
+ flex: 1,
280
+ type: columnType,
281
+ sortable: true
282
+ }, isRelation && {
283
+ relationDbType: field.dbType
284
+ });
285
+ return column;
286
+ });
287
+ columns.push({
288
+ field: "actions",
289
+ headerName: "",
290
+ type: "actions",
291
+ width: 100,
292
+ actions: [
293
+ {
294
+ action: "edit",
295
+ link: true
296
+ },
297
+ {
298
+ action: "delete"
299
+ }
300
+ ]
301
+ });
302
+ return columns;
303
+ }
304
+ function getColumnType(field) {
305
+ if (isRelationField(field)) {
306
+ return "relation";
307
+ }
308
+ switch (field.type) {
309
+ case "date":
310
+ return "date";
311
+ case "number":
312
+ return "number";
313
+ case "boolean":
314
+ return "boolean";
315
+ default:
316
+ return "text";
317
+ }
318
+ }
319
+ function pluralize(word) {
320
+ if (word.endsWith("y")) {
321
+ return word.slice(0, -1) + "ies";
322
+ }
323
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("ch") || word.endsWith("sh")) {
324
+ return word + "es";
325
+ }
326
+ return word + "s";
327
+ }
328
+ function deepMerge(defaults, custom) {
329
+ const result = __spreadValues({}, defaults);
330
+ for (const key in custom) {
331
+ const customValue = custom[key];
332
+ const defaultValue = defaults[key];
333
+ if (customValue === void 0 || customValue === null) {
334
+ continue;
335
+ }
336
+ if (typeof customValue === "object" && !Array.isArray(customValue) && typeof defaultValue === "object" && !Array.isArray(defaultValue) && defaultValue !== null) {
337
+ result[key] = deepMerge(
338
+ defaultValue,
339
+ customValue
340
+ );
341
+ } else {
342
+ result[key] = customValue;
343
+ }
344
+ }
345
+ return result;
346
+ }
347
+ function getColumnsFromFields(fields) {
348
+ return fields.filter((field) => field.displayInTable).map((field, i) => ({
349
+ type: i === 0 ? "naigateToDetails" : void 0,
350
+ field: field.name,
351
+ headerName: field.label || field.name,
352
+ flex: 1
353
+ }));
354
+ }
355
+ function createBrowserClient(supabaseUrl, supabaseAnonKey) {
356
+ return createBrowserClient$1(supabaseUrl, supabaseAnonKey);
357
+ }
358
+
359
+ // src/base/base-client.ts
360
+ var BasePublicAPIClient = class {
361
+ constructor(supabase, projectId, options = {}) {
362
+ this.configCache = /* @__PURE__ */ new Map();
363
+ var _a, _b;
364
+ this.supabase = supabase;
365
+ this.projectId = projectId;
366
+ this.enableCache = (_a = options.enableCache) != null ? _a : true;
367
+ this.cacheTTL = (_b = options.cacheTTL) != null ? _b : 5 * 60 * 1e3;
368
+ }
369
+ /**
370
+ * Загрузить fields для entityDefinition (с кэшированием)
371
+ *
372
+ * Флоу работы с кэшем:
373
+ *
374
+ * 1. Если enableCache === true (публичный API):
375
+ * - Проверяет кэш по entityDefinitionId
376
+ * - Если есть в кэше и не истек → возвращает из кэша (0 запросов к БД)
377
+ * - Если нет в кэше или истек → загружает из БД и кэширует (1 запрос к БД)
378
+ *
379
+ * 2. Если enableCache === false (админка):
380
+ * - Пропускает проверку кэша (эквивалент forceRefresh: true)
381
+ * - Всегда загружает из БД (1 запрос к БД)
382
+ * - Не сохраняет в кэш (всегда свежие данные)
383
+ *
384
+ * Используется внутри SDK для получения fields перед операциями с instances.
385
+ * Fields нужны для определения типов полей, relations, и уплощения данных.
386
+ */
387
+ async getFields(entityDefinitionId) {
388
+ const config = await this.getEntityDefinitionConfig(entityDefinitionId);
389
+ return config.fields;
390
+ }
391
+ /**
392
+ * Преобразование данных entity_definition из БД в EntityDefinitionConfig
393
+ */
394
+ transformEntityDefinitionFromDB(row) {
395
+ return {
396
+ id: row.id,
397
+ name: row.name,
398
+ description: row.description,
399
+ tableName: row.table_name,
400
+ type: row.type,
401
+ projectId: row.project_id,
402
+ createPermission: row.create_permission,
403
+ readPermission: row.read_permission,
404
+ updatePermission: row.update_permission,
405
+ deletePermission: row.delete_permission,
406
+ titleSection0: row.title_section_0,
407
+ titleSection1: row.title_section_1,
408
+ titleSection2: row.title_section_2,
409
+ titleSection3: row.title_section_3,
410
+ uiConfig: row.ui_config,
411
+ enablePagination: row.enable_pagination,
412
+ pageSize: row.page_size,
413
+ enableFilters: row.enable_filters,
414
+ maxFileSizeMb: row.max_file_size_mb,
415
+ maxFilesCount: row.max_files_count,
416
+ createdAt: row.created_at,
417
+ updatedAt: row.updated_at
418
+ };
419
+ }
420
+ /**
421
+ * Преобразование данных field из БД в FieldConfig
422
+ */
423
+ transformFieldFromDB(row) {
424
+ var _a;
425
+ return {
426
+ id: row.id,
427
+ entityDefinitionId: row.entity_definition_id,
428
+ name: row.name,
429
+ dbType: row.db_type,
430
+ type: row.type,
431
+ label: row.label,
432
+ placeholder: row.placeholder,
433
+ description: row.description,
434
+ forEditPage: row.for_edit_page,
435
+ forCreatePage: row.for_create_page,
436
+ required: row.required,
437
+ requiredText: row.required_text,
438
+ forEditPageDisabled: row.for_edit_page_disabled,
439
+ displayIndex: row.display_index,
440
+ displayInTable: row.display_in_table,
441
+ sectionIndex: (_a = row.section_index) != null ? _a : 0,
442
+ isOptionTitleField: row.is_option_title_field,
443
+ searchable: row.searchable,
444
+ filterableInList: row.filterable_in_list,
445
+ options: row.options,
446
+ relatedEntityDefinitionId: row.related_entity_definition_id,
447
+ relationFieldId: row.relation_field_id,
448
+ isRelationSource: row.is_relation_source,
449
+ selectorRelationId: row.selector_relation_id,
450
+ relationFieldName: row.relation_field_name,
451
+ relationFieldLabel: row.relation_field_label,
452
+ defaultStringValue: row.default_string_value,
453
+ defaultNumberValue: row.default_number_value,
454
+ defaultBooleanValue: row.default_boolean_value,
455
+ defaultDateValue: row.default_date_value,
456
+ autoPopulate: row.auto_populate,
457
+ includeInSinglePma: row.include_in_single_pma,
458
+ includeInListPma: row.include_in_list_pma,
459
+ includeInSingleSa: row.include_in_single_sa,
460
+ includeInListSa: row.include_in_list_sa,
461
+ foreignKey: row.foreign_key,
462
+ foreignKeyValue: row.foreign_key_value,
463
+ typeFieldName: row.type_field_name,
464
+ optionsFieldName: row.options_field_name,
465
+ acceptFileTypes: row.accept_file_types,
466
+ maxFileSize: row.max_file_size,
467
+ maxFiles: row.max_files,
468
+ storageBucket: row.storage_bucket,
469
+ createdAt: row.created_at,
470
+ updatedAt: row.updated_at
471
+ };
472
+ }
473
+ /**
474
+ * Загрузить entityDefinition с полями одним запросом (JOIN)
475
+ * Кэширует всю конфигурацию целиком (entityDefinition + fields)
476
+ *
477
+ * Логика кэширования:
478
+ * - enableCache: true → использует кэш, сохраняет в кэш (TTL: 5 минут по умолчанию)
479
+ * - enableCache: false → всегда загружает из БД, не использует кэш (эквивалент forceRefresh: true)
480
+ *
481
+ * @returns EntityDefinitionConfig с полями, отсортированными по display_index
482
+ */
483
+ async getEntityDefinitionConfig(entityDefinitionId) {
484
+ const forceRefresh = !this.enableCache;
485
+ if (!forceRefresh && this.enableCache) {
486
+ const cached = this.configCache.get(entityDefinitionId);
487
+ if (cached && Date.now() < cached.expiresAt) {
488
+ return cached.config;
489
+ }
490
+ }
491
+ const { data, error } = await this.supabase.from("entity_definition").select(
492
+ `
493
+ *,
494
+ field!field_entity_definition_id_fkey (*)
495
+ `
496
+ ).eq("id", entityDefinitionId).single();
497
+ if (error || !data) {
498
+ throw new Error(
499
+ `Entity definition not found: ${(error == null ? void 0 : error.message) || "Unknown error"}`
500
+ );
501
+ }
502
+ const entityDefinitionData = this.transformEntityDefinitionFromDB(data);
503
+ const fields = (data.field || []).sort((a, b) => {
504
+ var _a, _b;
505
+ const aIndex = (_a = a.display_index) != null ? _a : 999;
506
+ const bIndex = (_b = b.display_index) != null ? _b : 999;
507
+ return aIndex - bIndex;
508
+ }).map((row) => this.transformFieldFromDB(row));
509
+ const config = __spreadProps(__spreadValues({}, entityDefinitionData), {
510
+ fields
511
+ });
512
+ if (this.enableCache) {
513
+ this.configCache.set(entityDefinitionId, {
514
+ config,
515
+ expiresAt: Date.now() + this.cacheTTL
516
+ });
517
+ }
518
+ return config;
519
+ }
520
+ /**
521
+ * Преобразование EntityDefinitionConfig в EntityDefinition
522
+ */
523
+ convertToEntityDefinition(config) {
524
+ return {
525
+ id: config.id,
526
+ name: config.name,
527
+ description: config.description,
528
+ tableName: config.tableName,
529
+ type: config.type,
530
+ projectId: config.projectId,
531
+ createPermission: config.createPermission,
532
+ readPermission: config.readPermission,
533
+ updatePermission: config.updatePermission,
534
+ deletePermission: config.deletePermission,
535
+ titleSection0: config.titleSection0,
536
+ titleSection1: config.titleSection1,
537
+ titleSection2: config.titleSection2,
538
+ titleSection3: config.titleSection3,
539
+ uiConfig: config.uiConfig,
540
+ enablePagination: config.enablePagination,
541
+ pageSize: config.pageSize,
542
+ enableFilters: config.enableFilters,
543
+ maxFileSizeMb: config.maxFileSizeMb,
544
+ maxFilesCount: config.maxFilesCount,
545
+ createdAt: config.createdAt,
546
+ updatedAt: config.updatedAt
547
+ };
548
+ }
549
+ /**
550
+ * Преобразование FieldConfig в Field
551
+ */
552
+ convertToField(fieldConfig) {
553
+ return {
554
+ id: fieldConfig.id,
555
+ entityDefinitionId: fieldConfig.entityDefinitionId,
556
+ name: fieldConfig.name,
557
+ dbType: fieldConfig.dbType,
558
+ type: fieldConfig.type,
559
+ label: fieldConfig.label,
560
+ placeholder: fieldConfig.placeholder,
561
+ description: fieldConfig.description,
562
+ forEditPage: fieldConfig.forEditPage,
563
+ forCreatePage: fieldConfig.forCreatePage,
564
+ required: fieldConfig.required,
565
+ requiredText: fieldConfig.requiredText,
566
+ forEditPageDisabled: fieldConfig.forEditPageDisabled,
567
+ displayIndex: fieldConfig.displayIndex,
568
+ displayInTable: fieldConfig.displayInTable,
569
+ sectionIndex: fieldConfig.sectionIndex,
570
+ isOptionTitleField: fieldConfig.isOptionTitleField,
571
+ searchable: fieldConfig.searchable,
572
+ filterableInList: fieldConfig.filterableInList,
573
+ options: fieldConfig.options,
574
+ relatedEntityDefinitionId: fieldConfig.relatedEntityDefinitionId,
575
+ relationFieldId: fieldConfig.relationFieldId,
576
+ isRelationSource: fieldConfig.isRelationSource,
577
+ selectorRelationId: fieldConfig.selectorRelationId,
578
+ relationFieldName: fieldConfig.relationFieldName,
579
+ relationFieldLabel: fieldConfig.relationFieldLabel,
580
+ defaultStringValue: fieldConfig.defaultStringValue,
581
+ defaultNumberValue: fieldConfig.defaultNumberValue,
582
+ defaultBooleanValue: fieldConfig.defaultBooleanValue,
583
+ defaultDateValue: fieldConfig.defaultDateValue,
584
+ autoPopulate: fieldConfig.autoPopulate,
585
+ includeInSinglePma: fieldConfig.includeInSinglePma,
586
+ includeInListPma: fieldConfig.includeInListPma,
587
+ includeInSingleSa: fieldConfig.includeInSingleSa,
588
+ includeInListSa: fieldConfig.includeInListSa,
589
+ foreignKey: fieldConfig.foreignKey,
590
+ foreignKeyValue: fieldConfig.foreignKeyValue,
591
+ typeFieldName: fieldConfig.typeFieldName,
592
+ optionsFieldName: fieldConfig.optionsFieldName,
593
+ acceptFileTypes: fieldConfig.acceptFileTypes,
594
+ maxFileSize: fieldConfig.maxFileSize,
595
+ maxFiles: fieldConfig.maxFiles,
596
+ storageBucket: fieldConfig.storageBucket,
597
+ createdAt: fieldConfig.createdAt,
598
+ updatedAt: fieldConfig.updatedAt
599
+ };
600
+ }
601
+ /**
602
+ * Получить entity definition с полями и сгенерированным UI конфигом
603
+ * Использует кэш SDK для оптимизации (если дефиниция уже загружена, не делает повторный запрос)
604
+ *
605
+ * @param entityDefinitionId - ID entity definition
606
+ * @returns EntityDefinition, Fields и UI конфиг, или null если не найдено
607
+ */
608
+ async getEntityDefinitionWithUIConfig(entityDefinitionId) {
609
+ try {
610
+ const config = await this.getEntityDefinitionConfig(entityDefinitionId);
611
+ const entityDefinition = this.convertToEntityDefinition(config);
612
+ const fields = config.fields.map((f) => this.convertToField(f));
613
+ const uiConfig = generateUIConfig(entityDefinition, fields);
614
+ return {
615
+ entityDefinition,
616
+ fields,
617
+ uiConfig
618
+ };
619
+ } catch (error) {
620
+ if (error instanceof Error && error.message.includes("not found")) {
621
+ return null;
622
+ }
623
+ throw error;
624
+ }
625
+ }
626
+ /**
627
+ * Очистить кэш
628
+ */
629
+ clearCache() {
630
+ this.configCache.clear();
631
+ }
632
+ };
633
+
634
+ // src/utils/instance-utils.ts
635
+ function transformEntityInstance(row) {
636
+ return {
637
+ id: row.id,
638
+ entityDefinitionId: row.entity_definition_id,
639
+ projectId: row.project_id,
640
+ data: row.data || {},
641
+ createdAt: row.created_at,
642
+ updatedAt: row.updated_at
643
+ };
644
+ }
645
+ function flattenInstance(instance, fields, relationsAsIds = false) {
646
+ const result = {
647
+ id: instance.id,
648
+ entityDefinitionId: instance.entityDefinitionId,
649
+ projectId: instance.projectId,
650
+ createdAt: instance.createdAt,
651
+ updatedAt: instance.updatedAt
652
+ };
653
+ Object.entries(instance.data || {}).forEach(([key, value]) => {
654
+ result[key] = value;
655
+ });
656
+ if (instance.relations) {
657
+ Object.entries(instance.relations).forEach(
658
+ ([fieldName, relatedInstances]) => {
659
+ if (relationsAsIds) {
660
+ result[fieldName] = relatedInstances.map((inst) => inst.id);
661
+ } else {
662
+ const field = fields.find((f) => f.name === fieldName);
663
+ if ((field == null ? void 0 : field.dbType) === "manyToOne" || (field == null ? void 0 : field.dbType) === "oneToOne") {
664
+ result[fieldName] = relatedInstances[0] || null;
665
+ } else {
666
+ result[fieldName] = relatedInstances;
667
+ }
668
+ }
669
+ }
670
+ );
671
+ }
672
+ return result;
673
+ }
674
+
675
+ // src/client.ts
676
+ var _PublicAPIClient = class _PublicAPIClient extends BasePublicAPIClient {
677
+ constructor(supabase, projectId, mode, options = {}) {
678
+ super(supabase, projectId, options);
679
+ this.mode = mode;
680
+ }
681
+ /**
682
+ * Создать SDK клиент напрямую с Supabase инстансом
683
+ * Используется для создания через фабричные функции createServerSDK/createClientSDK
684
+ *
685
+ * @param supabase - Supabase клиент
686
+ * @param projectId - ID проекта
687
+ * @param mode - Режим работы: 'server' для SSR, 'client' для браузера
688
+ * @param options - Опции SDK (кэширование и т.д.)
689
+ */
690
+ static create(supabase, projectId, mode, options = {}) {
691
+ const cacheKey = `${mode}-${projectId}-${JSON.stringify(options)}`;
692
+ if (this.instances.has(cacheKey)) {
693
+ return this.instances.get(cacheKey);
694
+ }
695
+ const client = new _PublicAPIClient(supabase, projectId, mode, options);
696
+ this.instances.set(cacheKey, client);
697
+ return client;
698
+ }
699
+ /**
700
+ * Получить режим работы клиента (для отладки)
701
+ */
702
+ getMode() {
703
+ return this.mode;
704
+ }
705
+ /**
706
+ * Получить один экземпляр
707
+ */
708
+ async getInstance(entityDefinitionId, id, params) {
709
+ var _a;
710
+ try {
711
+ const fields = await this.getFields(entityDefinitionId);
712
+ const { data: instance, error: instanceError } = await this.supabase.from("entity_instance").select("*").eq("id", id).eq("entity_definition_id", entityDefinitionId).eq("project_id", this.projectId).single();
713
+ if (instanceError || !instance) {
714
+ handleInstanceError(
715
+ instanceError || new Error("Instance not found"),
716
+ id
717
+ );
718
+ }
719
+ const transformedInstance = transformEntityInstance(instance);
720
+ if (transformedInstance.entityDefinitionId !== entityDefinitionId) {
721
+ throw new NotFoundError("Entity instance", id);
722
+ }
723
+ const relationFields = fields.filter(
724
+ (f) => f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne"
725
+ );
726
+ const relations = {};
727
+ if (relationFields.length > 0) {
728
+ const relationFieldIds = relationFields.map((f) => f.id).filter((id2) => Boolean(id2));
729
+ if (relationFieldIds.length > 0) {
730
+ const { data: allRelations, error: relationsError } = await this.supabase.from("entity_relation").select("target_instance_id, relation_field_id").eq("source_instance_id", id).in("relation_field_id", relationFieldIds);
731
+ if (relationsError) {
732
+ throw new SDKError(
733
+ "RELATIONS_LOAD_ERROR",
734
+ `Failed to load relations for instance ${id}: ${relationsError.message}`,
735
+ 500,
736
+ relationsError
737
+ );
738
+ }
739
+ if (allRelations && allRelations.length > 0) {
740
+ const targetInstanceIds = [
741
+ ...new Set(allRelations.map((r) => r.target_instance_id))
742
+ ];
743
+ const { data: relatedInstances, error: instancesError } = await this.supabase.from("entity_instance").select("*").in("id", targetInstanceIds);
744
+ if (instancesError) {
745
+ throw new SDKError(
746
+ "RELATED_INSTANCES_LOAD_ERROR",
747
+ `Failed to load related instances for instance ${id}: ${instancesError.message}`,
748
+ 500,
749
+ instancesError
750
+ );
751
+ }
752
+ if (relatedInstances) {
753
+ const relatedInstancesMap = new Map(
754
+ relatedInstances.map((inst) => [
755
+ inst.id,
756
+ transformEntityInstance(inst)
757
+ ])
758
+ );
759
+ for (const relation of allRelations) {
760
+ const relationField = relationFields.find(
761
+ (f) => f.id === relation.relation_field_id
762
+ );
763
+ if (relationField) {
764
+ const relatedInstance = relatedInstancesMap.get(
765
+ relation.target_instance_id
766
+ );
767
+ if (relatedInstance) {
768
+ if (!relations[relationField.name]) {
769
+ relations[relationField.name] = [];
770
+ }
771
+ relations[relationField.name].push(
772
+ relatedInstance
773
+ );
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+ }
780
+ }
781
+ const fileFields = fields.filter(
782
+ (f) => f.type === "files" || f.type === "images"
783
+ );
784
+ if (fileFields.length > 0) {
785
+ const { data: allFiles, error: filesError } = await this.supabase.from("entity_file").select("id, field_id").eq("entity_instance_id", id);
786
+ if (filesError) {
787
+ throw new SDKError(
788
+ "FILES_LOAD_ERROR",
789
+ `Failed to load files for instance ${id}: ${filesError.message}`,
790
+ 500,
791
+ filesError
792
+ );
793
+ }
794
+ if (allFiles) {
795
+ const filesByFieldId = /* @__PURE__ */ new Map();
796
+ allFiles.forEach((file) => {
797
+ if (file.field_id) {
798
+ if (!filesByFieldId.has(file.field_id)) {
799
+ filesByFieldId.set(file.field_id, []);
800
+ }
801
+ filesByFieldId.get(file.field_id).push(file.id);
802
+ }
803
+ });
804
+ fileFields.forEach((field) => {
805
+ const fileIds = filesByFieldId.get(field.id) || [];
806
+ if (fileIds.length > 0 || !transformedInstance.data[field.name]) {
807
+ transformedInstance.data[field.name] = fileIds;
808
+ }
809
+ });
810
+ }
811
+ }
812
+ const instanceWithRelations = __spreadProps(__spreadValues({}, transformedInstance), {
813
+ relations: Object.keys(relations).length > 0 ? relations : void 0
814
+ });
815
+ return flattenInstance(
816
+ instanceWithRelations,
817
+ fields.map((f) => ({ name: f.name, dbType: f.dbType })),
818
+ (_a = params == null ? void 0 : params.relationsAsIds) != null ? _a : false
819
+ );
820
+ } catch (error) {
821
+ if (error instanceof NotFoundError || error instanceof PermissionDeniedError || error instanceof SDKError) {
822
+ throw error;
823
+ }
824
+ handleSupabaseError(error);
825
+ }
826
+ }
827
+ /**
828
+ * Получить список экземпляров
829
+ * Поддерживает поиск, фильтры (JSONB и relation), пагинацию
830
+ */
831
+ async getInstances(entityDefinitionId, params) {
832
+ var _a, _b;
833
+ try {
834
+ const fields = await this.getFields(entityDefinitionId);
835
+ const page = (params == null ? void 0 : params.page) || 1;
836
+ const limit = (params == null ? void 0 : params.limit) || 20;
837
+ const offset = (page - 1) * limit;
838
+ const relationFieldsMap = /* @__PURE__ */ new Map();
839
+ fields.forEach((field) => {
840
+ if (field.relatedEntityDefinitionId && (field.dbType === "manyToMany" || field.dbType === "manyToOne" || field.dbType === "oneToMany" || field.dbType === "oneToOne")) {
841
+ relationFieldsMap.set(field.name, { fieldId: field.id });
842
+ }
843
+ });
844
+ const jsonbFilters = {};
845
+ const relationFiltersToApply = [];
846
+ if (params == null ? void 0 : params.filters) {
847
+ Object.entries(params.filters).forEach(([fieldName, values]) => {
848
+ if (values && values.length > 0) {
849
+ const relationField = relationFieldsMap.get(fieldName);
850
+ if (relationField) {
851
+ relationFiltersToApply.push({
852
+ fieldName,
853
+ fieldId: relationField.fieldId,
854
+ values
855
+ });
856
+ } else {
857
+ jsonbFilters[fieldName] = values;
858
+ }
859
+ }
860
+ });
861
+ }
862
+ let allowedInstanceIds = null;
863
+ if (relationFiltersToApply.length > 0) {
864
+ const filterModes = (params == null ? void 0 : params.relationFilterModes) || {};
865
+ const anyModeFilters = [];
866
+ const allModeFilters = [];
867
+ relationFiltersToApply.forEach((rf) => {
868
+ const mode = filterModes[rf.fieldName] || "any";
869
+ if (mode === "all") {
870
+ allModeFilters.push(rf);
871
+ } else {
872
+ anyModeFilters.push(rf);
873
+ }
874
+ });
875
+ const anyModeInstanceIds = anyModeFilters.length > 0 ? /* @__PURE__ */ new Set() : null;
876
+ if (anyModeInstanceIds !== null && anyModeFilters.length > 0) {
877
+ const orConditions = anyModeFilters.flatMap(
878
+ (rf) => rf.values.map(
879
+ (targetId) => `and(relation_field_id.eq.${rf.fieldId},target_instance_id.eq.${targetId})`
880
+ )
881
+ );
882
+ const { data: relations, error: relError } = await this.supabase.from("entity_relation").select("source_instance_id").or(orConditions.join(","));
883
+ if (relError) {
884
+ throw new SDKError(
885
+ "RELATION_FILTER_ERROR",
886
+ `Failed to apply relation filters (any mode): ${relError.message}`,
887
+ 500,
888
+ relError
889
+ );
890
+ } else if (relations) {
891
+ relations.forEach(
892
+ (r) => anyModeInstanceIds.add(r.source_instance_id)
893
+ );
894
+ }
895
+ }
896
+ const allModeInstanceIdSets = [];
897
+ for (const rf of allModeFilters) {
898
+ const { data: relations, error: relError } = await this.supabase.from("entity_relation").select("source_instance_id, target_instance_id").eq("relation_field_id", rf.fieldId).in("target_instance_id", rf.values);
899
+ if (relError) {
900
+ throw new SDKError(
901
+ "RELATION_FILTER_ERROR",
902
+ `Failed to apply relation filters (all mode): ${relError.message}`,
903
+ 500,
904
+ relError
905
+ );
906
+ }
907
+ if (relations) {
908
+ const sourceTargetMap = /* @__PURE__ */ new Map();
909
+ relations.forEach((r) => {
910
+ if (!sourceTargetMap.has(r.source_instance_id)) {
911
+ sourceTargetMap.set(r.source_instance_id, /* @__PURE__ */ new Set());
912
+ }
913
+ sourceTargetMap.get(r.source_instance_id).add(r.target_instance_id);
914
+ });
915
+ const validSources = /* @__PURE__ */ new Set();
916
+ const requiredTargets = new Set(rf.values);
917
+ sourceTargetMap.forEach((targets, sourceId) => {
918
+ const hasAll = [...requiredTargets].every((t) => targets.has(t));
919
+ if (hasAll) {
920
+ validSources.add(sourceId);
921
+ }
922
+ });
923
+ allModeInstanceIdSets.push(validSources);
924
+ }
925
+ }
926
+ if (anyModeInstanceIds !== null || allModeInstanceIdSets.length > 0) {
927
+ if (anyModeInstanceIds !== null && anyModeInstanceIds.size > 0) {
928
+ allowedInstanceIds = [...anyModeInstanceIds];
929
+ } else if (allModeInstanceIdSets.length > 0) {
930
+ allowedInstanceIds = [...allModeInstanceIdSets[0]];
931
+ }
932
+ if (allowedInstanceIds !== null && allModeInstanceIdSets.length > 0) {
933
+ for (const idSet of allModeInstanceIdSets) {
934
+ allowedInstanceIds = allowedInstanceIds.filter(
935
+ (id) => idSet.has(id)
936
+ );
937
+ }
938
+ }
939
+ }
940
+ if (allowedInstanceIds !== null && allowedInstanceIds.length === 0) {
941
+ return {
942
+ data: [],
943
+ pagination: {
944
+ page,
945
+ limit,
946
+ total: 0,
947
+ totalPages: 0,
948
+ hasPreviousPage: false,
949
+ hasNextPage: false
950
+ }
951
+ };
952
+ }
953
+ }
954
+ const searchFields = (() => {
955
+ const searchable = fields.filter((f) => f.searchable).map((f) => f.name);
956
+ return searchable.length > 0 ? searchable : ["name"];
957
+ })();
958
+ const searchTerm = ((_a = params == null ? void 0 : params.search) == null ? void 0 : _a.trim()) || null;
959
+ let data = null;
960
+ let error = null;
961
+ let count = null;
962
+ if (searchTerm) {
963
+ const rpcParams = {
964
+ p_entity_definition_id: entityDefinitionId,
965
+ p_project_id: this.projectId,
966
+ p_search_term: searchTerm,
967
+ p_search_fields: searchFields,
968
+ p_limit: limit,
969
+ p_offset: offset
970
+ };
971
+ const { data: rpcData, error: rpcError } = await this.supabase.rpc(
972
+ "search_entity_instances",
973
+ rpcParams
974
+ );
975
+ if (rpcError) {
976
+ error = rpcError;
977
+ } else if (rpcData && rpcData.length > 0) {
978
+ count = rpcData[0].total_count;
979
+ data = rpcData;
980
+ } else {
981
+ data = [];
982
+ count = 0;
983
+ }
984
+ } else {
985
+ let query = this.supabase.from("entity_instance").select("*", { count: "exact" }).eq("entity_definition_id", entityDefinitionId).eq("project_id", this.projectId);
986
+ if (allowedInstanceIds !== null && allowedInstanceIds.length > 0) {
987
+ query = query.in("id", allowedInstanceIds);
988
+ }
989
+ if (Object.keys(jsonbFilters).length > 0) {
990
+ Object.entries(jsonbFilters).forEach(([fieldName, values]) => {
991
+ if (values && values.length > 0) {
992
+ query = query.or(
993
+ values.map((v) => `data->>${fieldName}.eq.${v}`).join(",")
994
+ );
995
+ }
996
+ });
997
+ }
998
+ const sortBy = (params == null ? void 0 : params.sortBy) || "created_at";
999
+ const sortOrder = (params == null ? void 0 : params.sortOrder) || "desc";
1000
+ query = query.order(sortBy, { ascending: sortOrder === "asc" });
1001
+ query = query.range(offset, offset + limit - 1);
1002
+ const result = await query;
1003
+ data = result.data;
1004
+ error = result.error;
1005
+ count = result.count;
1006
+ }
1007
+ if (error) {
1008
+ handleSupabaseError(error);
1009
+ }
1010
+ if (!data || data.length === 0) {
1011
+ return {
1012
+ data: [],
1013
+ pagination: {
1014
+ page,
1015
+ limit,
1016
+ total: count || 0,
1017
+ totalPages: Math.ceil((count || 0) / limit),
1018
+ hasPreviousPage: page > 1,
1019
+ hasNextPage: page < Math.ceil((count || 0) / limit)
1020
+ }
1021
+ };
1022
+ }
1023
+ const transformedInstances = data.map(transformEntityInstance);
1024
+ const relationsMap = /* @__PURE__ */ new Map();
1025
+ const relationFieldsToLoad = fields.filter(
1026
+ (f) => f.relatedEntityDefinitionId && (f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne") && f.displayInTable
1027
+ ).map((f) => f.name);
1028
+ if (relationFieldsToLoad.length > 0 && transformedInstances.length > 0) {
1029
+ const relationFields = relationFieldsToLoad.map((fieldName) => {
1030
+ const field = fields.find(
1031
+ (f) => f.name === fieldName && (f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne")
1032
+ );
1033
+ return field ? { name: fieldName, field } : null;
1034
+ }).filter(
1035
+ (f) => f !== null
1036
+ );
1037
+ if (relationFields.length > 0) {
1038
+ const instanceIds = transformedInstances.map((inst) => inst.id);
1039
+ const relationFieldIds = relationFields.map((rf) => rf.field.id);
1040
+ const { data: relationsWithInstances, error: rpcError } = await this.supabase.rpc(
1041
+ "get_related_instances",
1042
+ {
1043
+ p_source_instance_ids: instanceIds,
1044
+ p_relation_field_ids: relationFieldIds
1045
+ }
1046
+ );
1047
+ if (rpcError) {
1048
+ throw new SDKError(
1049
+ "RELATIONS_LOAD_ERROR",
1050
+ `Failed to load relations with instances: ${rpcError.message}`,
1051
+ 500,
1052
+ rpcError
1053
+ );
1054
+ }
1055
+ if (relationsWithInstances && relationsWithInstances.length > 0) {
1056
+ const relatedInstancesMap = new Map(
1057
+ relationsWithInstances.map((row) => [
1058
+ row.target_instance_id,
1059
+ transformEntityInstance({
1060
+ id: row.target_instance_id,
1061
+ entity_definition_id: row.target_entity_definition_id,
1062
+ project_id: row.target_project_id,
1063
+ data: row.target_data,
1064
+ created_at: row.target_created_at,
1065
+ updated_at: row.target_updated_at
1066
+ })
1067
+ ])
1068
+ );
1069
+ const targetEntityDefinitionIds = [
1070
+ ...new Set(
1071
+ Array.from(relatedInstancesMap.values()).map(
1072
+ (inst) => inst.entityDefinitionId
1073
+ )
1074
+ )
1075
+ ];
1076
+ const fieldsMap = /* @__PURE__ */ new Map();
1077
+ await Promise.all(
1078
+ targetEntityDefinitionIds.map(async (entityDefId) => {
1079
+ const targetFields = await this.getFields(entityDefId);
1080
+ fieldsMap.set(
1081
+ entityDefId,
1082
+ targetFields.map((f) => ({
1083
+ name: f.name,
1084
+ dbType: f.dbType
1085
+ }))
1086
+ );
1087
+ })
1088
+ );
1089
+ for (const relationRow of relationsWithInstances) {
1090
+ const instanceId = relationRow.source_instance_id;
1091
+ const relationField = relationFields.find(
1092
+ (rf) => rf.field.id === relationRow.relation_field_id
1093
+ );
1094
+ if (relationField) {
1095
+ const relatedInstance = relatedInstancesMap.get(
1096
+ relationRow.target_instance_id
1097
+ );
1098
+ if (relatedInstance) {
1099
+ if (!relationsMap.has(instanceId)) {
1100
+ relationsMap.set(instanceId, {});
1101
+ }
1102
+ const instanceRelations = relationsMap.get(instanceId);
1103
+ if (!instanceRelations[relationField.name]) {
1104
+ instanceRelations[relationField.name] = [];
1105
+ }
1106
+ const targetFields = fieldsMap.get(relatedInstance.entityDefinitionId) || [];
1107
+ const flattenedRelated = flattenInstance(
1108
+ relatedInstance,
1109
+ targetFields,
1110
+ (_b = params == null ? void 0 : params.relationsAsIds) != null ? _b : false
1111
+ );
1112
+ instanceRelations[relationField.name].push(flattenedRelated);
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+ const fileFields = fields.filter(
1120
+ (f) => f.type === "files" || f.type === "images"
1121
+ );
1122
+ if (fileFields.length > 0 && transformedInstances.length > 0) {
1123
+ const instanceIds = transformedInstances.map((inst) => inst.id);
1124
+ const fieldIds = fileFields.map((f) => f.id);
1125
+ const { data: allFiles, error: filesError } = await this.supabase.from("entity_file").select("id, entity_instance_id, field_id").in("entity_instance_id", instanceIds).in("field_id", fieldIds);
1126
+ if (filesError) {
1127
+ throw new SDKError(
1128
+ "FILES_LOAD_ERROR",
1129
+ `Failed to load files: ${filesError.message}`,
1130
+ 500,
1131
+ filesError
1132
+ );
1133
+ }
1134
+ if (allFiles) {
1135
+ const filesMap = /* @__PURE__ */ new Map();
1136
+ allFiles.forEach((file) => {
1137
+ if (file.field_id) {
1138
+ if (!filesMap.has(file.entity_instance_id)) {
1139
+ filesMap.set(file.entity_instance_id, /* @__PURE__ */ new Map());
1140
+ }
1141
+ const instanceFiles = filesMap.get(file.entity_instance_id);
1142
+ if (!instanceFiles.has(file.field_id)) {
1143
+ instanceFiles.set(file.field_id, []);
1144
+ }
1145
+ instanceFiles.get(file.field_id).push(file.id);
1146
+ }
1147
+ });
1148
+ transformedInstances.forEach((instance) => {
1149
+ const instanceFiles = filesMap.get(instance.id);
1150
+ if (instanceFiles) {
1151
+ fileFields.forEach((field) => {
1152
+ const fileIds = instanceFiles.get(field.id) || [];
1153
+ if (fileIds.length > 0 || !instance.data[field.name]) {
1154
+ instance.data[field.name] = fileIds;
1155
+ }
1156
+ });
1157
+ }
1158
+ });
1159
+ }
1160
+ }
1161
+ const instancesWithRelations = transformedInstances.map((instance) => {
1162
+ const instanceRelations = relationsMap.get(instance.id) || {};
1163
+ return __spreadProps(__spreadValues({}, instance), {
1164
+ relations: Object.keys(instanceRelations).length > 0 ? instanceRelations : void 0
1165
+ });
1166
+ });
1167
+ const flattenedInstances = instancesWithRelations.map(
1168
+ (inst) => {
1169
+ var _a2;
1170
+ return flattenInstance(
1171
+ inst,
1172
+ fields.map((f) => ({ name: f.name, dbType: f.dbType })),
1173
+ (_a2 = params == null ? void 0 : params.relationsAsIds) != null ? _a2 : false
1174
+ );
1175
+ }
1176
+ );
1177
+ const total = count || 0;
1178
+ const totalPages = Math.ceil(total / limit);
1179
+ return {
1180
+ data: flattenedInstances,
1181
+ pagination: {
1182
+ page,
1183
+ limit,
1184
+ total,
1185
+ totalPages,
1186
+ hasPreviousPage: page > 1,
1187
+ hasNextPage: page < totalPages
1188
+ }
1189
+ };
1190
+ } catch (error) {
1191
+ if (error instanceof NotFoundError || error instanceof PermissionDeniedError || error instanceof SDKError) {
1192
+ throw error;
1193
+ }
1194
+ handleSupabaseError(error);
1195
+ }
1196
+ }
1197
+ /**
1198
+ * Создать экземпляр сущности
1199
+ * Поддерживает создание с relations и автоматически устанавливает created_by
1200
+ */
1201
+ async createInstance(entityDefinitionId, data) {
1202
+ try {
1203
+ const fields = await this.getFields(entityDefinitionId);
1204
+ const {
1205
+ data: { user },
1206
+ error: userError
1207
+ } = await this.supabase.auth.getUser();
1208
+ if (userError) {
1209
+ console.warn(
1210
+ "[SDK] Could not get user for created_by:",
1211
+ userError.message
1212
+ );
1213
+ }
1214
+ const { data: instanceData, relations } = data;
1215
+ const { data: instance, error: instanceError } = await this.supabase.from("entity_instance").insert({
1216
+ entity_definition_id: entityDefinitionId,
1217
+ project_id: this.projectId,
1218
+ data: instanceData,
1219
+ created_by: (user == null ? void 0 : user.id) || null
1220
+ }).select().single();
1221
+ if (instanceError || !instance) {
1222
+ handleInstanceError(
1223
+ instanceError || new Error("Failed to create instance"),
1224
+ "new"
1225
+ );
1226
+ }
1227
+ const transformedInstance = transformEntityInstance(instance);
1228
+ if (relations && Object.keys(relations).length > 0) {
1229
+ const relationFields = fields.filter(
1230
+ (f) => f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne"
1231
+ );
1232
+ const relationInserts = [];
1233
+ for (const [fieldName, targetInstanceIds] of Object.entries(
1234
+ relations
1235
+ )) {
1236
+ const field = relationFields.find((f) => f.name === fieldName);
1237
+ if (!field) {
1238
+ console.warn(
1239
+ `[SDK] Field ${fieldName} not found or not a relation field`
1240
+ );
1241
+ continue;
1242
+ }
1243
+ const ids = Array.isArray(targetInstanceIds) ? targetInstanceIds : targetInstanceIds ? [targetInstanceIds] : [];
1244
+ for (const targetInstanceId of ids) {
1245
+ if (targetInstanceId) {
1246
+ relationInserts.push({
1247
+ source_instance_id: transformedInstance.id,
1248
+ target_instance_id: targetInstanceId,
1249
+ relation_field_id: field.id,
1250
+ relation_type: field.dbType
1251
+ });
1252
+ }
1253
+ }
1254
+ }
1255
+ if (relationInserts.length > 0) {
1256
+ const { error: relationsError } = await this.supabase.from("entity_relation").insert(relationInserts);
1257
+ if (relationsError) {
1258
+ throw new SDKError(
1259
+ "RELATIONS_CREATE_ERROR",
1260
+ `Failed to create relations: ${relationsError.message}`,
1261
+ 500,
1262
+ relationsError
1263
+ );
1264
+ }
1265
+ }
1266
+ }
1267
+ return await this.getInstance(entityDefinitionId, transformedInstance.id);
1268
+ } catch (error) {
1269
+ if (error instanceof NotFoundError || error instanceof PermissionDeniedError || error instanceof SDKError) {
1270
+ throw error;
1271
+ }
1272
+ handleSupabaseError(error);
1273
+ }
1274
+ }
1275
+ async updateInstance(entityDefinitionId, id, data) {
1276
+ try {
1277
+ const { data: currentInstance, error: checkError } = await this.supabase.from("entity_instance").select("id, data, entity_definition_id, project_id").eq("id", id).eq("entity_definition_id", entityDefinitionId).eq("project_id", this.projectId).single();
1278
+ if (checkError || !currentInstance) {
1279
+ handleInstanceError(checkError || new Error("Instance not found"), id);
1280
+ }
1281
+ const fields = await this.getFields(entityDefinitionId);
1282
+ const { data: instanceData, relations } = data;
1283
+ const updatedData = __spreadValues(__spreadValues({}, currentInstance.data || {}), instanceData);
1284
+ const { data: updated, error: updateError } = await this.supabase.from("entity_instance").update({
1285
+ data: updatedData,
1286
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1287
+ }).eq("id", id).eq("entity_definition_id", entityDefinitionId).eq("project_id", this.projectId).select().single();
1288
+ if (updateError || !updated) {
1289
+ throw new SDKError(
1290
+ "UPDATE_ERROR",
1291
+ `Failed to update entity instance: ${(updateError == null ? void 0 : updateError.message) || "Update failed"}`,
1292
+ 500,
1293
+ updateError
1294
+ );
1295
+ }
1296
+ if (relations && Object.keys(relations).length > 0) {
1297
+ const relationFields = fields.filter(
1298
+ (f) => f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne"
1299
+ );
1300
+ const fieldIds = Object.keys(relations).map((fieldName) => {
1301
+ const field = relationFields.find((f) => f.name === fieldName);
1302
+ return field == null ? void 0 : field.id;
1303
+ }).filter(Boolean);
1304
+ if (fieldIds.length > 0) {
1305
+ const { error: deleteError } = await this.supabase.from("entity_relation").delete().eq("source_instance_id", id).in("relation_field_id", fieldIds);
1306
+ if (deleteError) {
1307
+ throw new SDKError(
1308
+ "RELATIONS_DELETE_ERROR",
1309
+ `Failed to delete old relations: ${deleteError.message}`,
1310
+ 500,
1311
+ deleteError
1312
+ );
1313
+ }
1314
+ }
1315
+ const relationInserts = [];
1316
+ for (const [fieldName, targetInstanceIds] of Object.entries(
1317
+ relations
1318
+ )) {
1319
+ const field = relationFields.find((f) => f.name === fieldName);
1320
+ if (!field) {
1321
+ console.warn(
1322
+ `[SDK] Field ${fieldName} not found or not a relation field`
1323
+ );
1324
+ continue;
1325
+ }
1326
+ const ids = Array.isArray(targetInstanceIds) ? targetInstanceIds : targetInstanceIds ? [targetInstanceIds] : [];
1327
+ for (const targetInstanceId of ids) {
1328
+ if (targetInstanceId) {
1329
+ relationInserts.push({
1330
+ source_instance_id: id,
1331
+ target_instance_id: targetInstanceId,
1332
+ relation_field_id: field.id,
1333
+ relation_type: field.dbType
1334
+ });
1335
+ }
1336
+ }
1337
+ }
1338
+ if (relationInserts.length > 0) {
1339
+ const { error: relationsError } = await this.supabase.from("entity_relation").insert(relationInserts);
1340
+ if (relationsError) {
1341
+ throw new SDKError(
1342
+ "RELATIONS_CREATE_ERROR",
1343
+ `Failed to create relations: ${relationsError.message}`,
1344
+ 500,
1345
+ relationsError
1346
+ );
1347
+ }
1348
+ }
1349
+ }
1350
+ return await this.buildUpdatedInstance(updated, fields, relations || {});
1351
+ } catch (error) {
1352
+ if (error instanceof NotFoundError || error instanceof PermissionDeniedError || error instanceof SDKError) {
1353
+ throw error;
1354
+ }
1355
+ handleSupabaseError(error);
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Формирует обновленный экземпляр из уже обновленных данных без повторной загрузки
1360
+ * Оптимизация: избегает лишних запросов после updateInstance
1361
+ * Загружает полные объекты relations для совместимости с форматом списка
1362
+ */
1363
+ async buildUpdatedInstance(updated, fields, relations) {
1364
+ const transformedInstance = transformEntityInstance(updated);
1365
+ const relationsMap = {};
1366
+ if (Object.keys(relations).length > 0) {
1367
+ const relationFields = fields.filter(
1368
+ (f) => f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne"
1369
+ );
1370
+ const allTargetInstanceIds = [
1371
+ ...new Set(
1372
+ Object.values(relations).flatMap(
1373
+ (ids) => Array.isArray(ids) ? ids : [ids]
1374
+ )
1375
+ )
1376
+ ].filter(Boolean);
1377
+ if (allTargetInstanceIds.length > 0) {
1378
+ const { data: relatedInstances, error: instancesError } = await this.supabase.from("entity_instance").select("*").in("id", allTargetInstanceIds);
1379
+ if (instancesError) {
1380
+ console.warn(
1381
+ "[SDK] Failed to load related instances for buildUpdatedInstance:",
1382
+ instancesError.message
1383
+ );
1384
+ } else if (relatedInstances) {
1385
+ const relatedInstancesMap = new Map(
1386
+ relatedInstances.map((inst) => [
1387
+ inst.id,
1388
+ transformEntityInstance(inst)
1389
+ ])
1390
+ );
1391
+ const targetEntityDefinitionIds = [
1392
+ ...new Set(
1393
+ Array.from(relatedInstancesMap.values()).map(
1394
+ (inst) => inst.entityDefinitionId
1395
+ )
1396
+ )
1397
+ ];
1398
+ const fieldsMap = /* @__PURE__ */ new Map();
1399
+ await Promise.all(
1400
+ targetEntityDefinitionIds.map(async (entityDefId) => {
1401
+ const targetFields = await this.getFields(entityDefId);
1402
+ fieldsMap.set(
1403
+ entityDefId,
1404
+ targetFields.map((f) => ({
1405
+ name: f.name,
1406
+ dbType: f.dbType
1407
+ }))
1408
+ );
1409
+ })
1410
+ );
1411
+ for (const [fieldName, targetInstanceIds] of Object.entries(
1412
+ relations
1413
+ )) {
1414
+ const field = relationFields.find((f) => f.name === fieldName);
1415
+ if (field) {
1416
+ const ids = Array.isArray(targetInstanceIds) ? targetInstanceIds : targetInstanceIds ? [targetInstanceIds] : [];
1417
+ const relatedInstancesForField = [];
1418
+ for (const id of ids) {
1419
+ const relatedInstance = relatedInstancesMap.get(id);
1420
+ if (relatedInstance) {
1421
+ const targetFields = fieldsMap.get(relatedInstance.entityDefinitionId) || [];
1422
+ const flattenedRelated = flattenInstance(
1423
+ relatedInstance,
1424
+ targetFields,
1425
+ false
1426
+ // relationsAsIds = false для совместимости со списком
1427
+ );
1428
+ relatedInstancesForField.push(flattenedRelated);
1429
+ }
1430
+ }
1431
+ if (relatedInstancesForField.length > 0) {
1432
+ relationsMap[fieldName] = relatedInstancesForField;
1433
+ }
1434
+ }
1435
+ }
1436
+ }
1437
+ }
1438
+ }
1439
+ const instanceWithRelations = __spreadProps(__spreadValues({}, transformedInstance), {
1440
+ relations: Object.keys(relationsMap).length > 0 ? relationsMap : void 0
1441
+ });
1442
+ return flattenInstance(
1443
+ instanceWithRelations,
1444
+ fields.map((f) => ({ name: f.name, dbType: f.dbType })),
1445
+ false
1446
+ // relationsAsIds = false - возвращаем полные объекты как в списке
1447
+ );
1448
+ }
1449
+ /**
1450
+ * Удалить экземпляр сущности
1451
+ * Связи удалятся автоматически через ON DELETE CASCADE
1452
+ */
1453
+ async deleteInstance(entityDefinitionId, id) {
1454
+ try {
1455
+ const { data: instance, error: checkError } = await this.supabase.from("entity_instance").select("id, entity_definition_id, project_id").eq("id", id).eq("entity_definition_id", entityDefinitionId).eq("project_id", this.projectId).single();
1456
+ if (checkError || !instance) {
1457
+ handleInstanceError(checkError || new Error("Instance not found"), id);
1458
+ }
1459
+ const { error: deleteError } = await this.supabase.from("entity_instance").delete().eq("id", id).eq("entity_definition_id", entityDefinitionId).eq("project_id", this.projectId);
1460
+ if (deleteError) {
1461
+ throw new SDKError(
1462
+ "DELETE_ERROR",
1463
+ `Failed to delete entity instance: ${deleteError.message}`,
1464
+ 500,
1465
+ deleteError
1466
+ );
1467
+ }
1468
+ } catch (error) {
1469
+ if (error instanceof NotFoundError || error instanceof PermissionDeniedError || error instanceof SDKError) {
1470
+ throw error;
1471
+ }
1472
+ handleSupabaseError(error);
1473
+ }
1474
+ }
1475
+ async signIn(email, password) {
1476
+ throw new Error("signIn: Not implemented yet");
1477
+ }
1478
+ async signUp(data) {
1479
+ throw new Error("signUp: Not implemented yet");
1480
+ }
1481
+ async signOut() {
1482
+ throw new Error("signOut: Not implemented yet");
1483
+ }
1484
+ async getCurrentUser() {
1485
+ throw new Error("getCurrentUser: Not implemented yet");
1486
+ }
1487
+ };
1488
+ _PublicAPIClient.instances = /* @__PURE__ */ new Map();
1489
+ var PublicAPIClient = _PublicAPIClient;
1490
+
1491
+ // src/browser.ts
1492
+ function createClientSDK(projectId, config, options) {
1493
+ const supabase = createBrowserClient(
1494
+ config.supabaseUrl,
1495
+ config.supabaseAnonKey
1496
+ );
1497
+ return PublicAPIClient.create(supabase, projectId, "client", options);
1498
+ }
1499
+
1500
+ export { AuthenticationError, NotFoundError, PermissionDeniedError, PublicAPIClient, SDKError, ValidationError, createClientSDK, generateUIConfig, getColumnsFromFields, getFieldValue, handleInstanceError, handleSupabaseError, isEntityData, isFieldValue, normalizeFieldValue, toCamelCase, toSnakeCase, validateInstanceData };
1501
+ //# sourceMappingURL=index.mjs.map
1502
+ //# sourceMappingURL=index.mjs.map