@igorchugurov/public-api-sdk 1.0.0 → 1.2.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/README.md CHANGED
@@ -12,6 +12,56 @@ pnpm add @igorchugurov/public-api-sdk@workspace:*
12
12
  pnpm add @igorchugurov/public-api-sdk
13
13
  ```
14
14
 
15
+ ## ✨ Возможности
16
+
17
+ SDK предоставляет полный набор инструментов для работы с универсальными сущностями:
18
+
19
+ ### 🔄 CRUD операции
20
+ - **Получение списка** с расширенной фильтрацией, поиском и пагинацией
21
+ - **Получение одного экземпляра** с автоматической загрузкой связей и файлов
22
+ - **Создание** экземпляров с поддержкой relations и автоматической установкой `created_by`
23
+ - **Обновление** экземпляров с поддержкой частичных обновлений и relations
24
+ - **Удаление** экземпляров с автоматической очисткой связей
25
+
26
+ ### 🔗 Работа со связями (Relations)
27
+ - Автоматическое определение relation-полей из конфигурации
28
+ - Поддержка всех типов связей: `manyToMany`, `manyToOne`, `oneToMany`, `oneToOne`
29
+ - Batch-загрузка связанных объектов для оптимизации производительности
30
+ - Фильтрация по relations с режимами `any` (OR) и `all` (AND)
31
+ - Опция получения relations как ID или полных объектов
32
+
33
+ ### 🔍 Поиск и фильтрация
34
+ - **Умный поиск** по полям с флагом `searchable: true`
35
+ - **JSONB фильтрация** для обычных полей
36
+ - **Relation фильтрация** с автоматическим определением relation-полей
37
+ - **Гибкая сортировка** по любому полю с поддержкой `asc`/`desc`
38
+
39
+ ### 📁 Работа с файлами
40
+ - Автоматическая загрузка файлов и изображений для полей типа `files` и `images`
41
+ - Batch-загрузка файлов для оптимизации запросов
42
+ - Поддержка множественных файлов на поле
43
+
44
+ ### ⚡ Производительность
45
+ - **Кэширование конфигурации** EntityDefinition и Fields (TTL: 5 минут по умолчанию)
46
+ - **Batch-запросы** для relations и файлов
47
+ - **Оптимизированные RPC функции** для поиска и загрузки связей
48
+ - Поддержка ESM и CJS форматов
49
+
50
+ ### 🎯 Типобезопасность
51
+ - Полная типизация всех методов и параметров
52
+ - Экспорт всех типов для использования в вашем коде
53
+ - Типизированные ошибки для удобной обработки
54
+
55
+ ### 🔐 Безопасность
56
+ - Интеграция с Supabase RLS (Row Level Security)
57
+ - Автоматическая проверка прав доступа
58
+ - Типизированные ошибки для различных сценариев доступа
59
+
60
+ ### 🎨 UI конфигурация
61
+ - Автоматическая генерация UI конфигурации из EntityDefinition и Fields
62
+ - Поддержка кастомных UI настроек
63
+ - Генерация конфигурации колонок таблицы из полей
64
+
15
65
  ## 🚀 Быстрый старт
16
66
 
17
67
  ### 1. Server Component (SSR)
@@ -102,7 +152,7 @@ const { data, pagination } = await sdk.getInstances(entityDefinitionId, {
102
152
 
103
153
  #### `getInstance(entityDefinitionId, id, params?)`
104
154
 
105
- Получить один экземпляр.
155
+ Получить один экземпляр по ID.
106
156
 
107
157
  ```typescript
108
158
  const instance = await sdk.getInstance(entityDefinitionId, id, {
@@ -110,17 +160,57 @@ const instance = await sdk.getInstance(entityDefinitionId, id, {
110
160
  });
111
161
  ```
112
162
 
163
+ **Пример:**
164
+
165
+ ```typescript
166
+ const instance = await sdk.getInstance("entity-def-id", "instance-id", {
167
+ relationsAsIds: true, // для редактирования - нужны только ID
168
+ });
169
+ ```
170
+
171
+ #### `getInstanceBySlug(entityDefinitionId, slug, params?)`
172
+
173
+ Получить один экземпляр по slug.
174
+
175
+ ```typescript
176
+ const instance = await sdk.getInstanceBySlug(entityDefinitionId, slug, {
177
+ relationsAsIds?: boolean; // default: false
178
+ });
179
+ ```
180
+
181
+ **Пример:**
182
+
183
+ ```typescript
184
+ const instance = await sdk.getInstanceBySlug("entity-def-id", "my-article-slug", {
185
+ relationsAsIds: false, // для отображения - нужны полные объекты
186
+ });
187
+ ```
188
+
189
+ **Особенности:**
190
+ - Валидирует формат slug перед запросом (только строчные латинские буквы, цифры и дефисы)
191
+ - Работает аналогично `getInstance`, но ищет по slug вместо id
192
+ - Поддерживает те же параметры, что и `getInstance` (relationsAsIds)
193
+ - Параметр `relationsAsIds`:
194
+ - `false` (по умолчанию) - возвращает полные объекты связанных сущностей
195
+ - `true` - возвращает только массивы ID связанных сущностей
196
+
113
197
  #### `createInstance(entityDefinitionId, data)`
114
198
 
115
- Создать новый экземпляр.
199
+ Создать новый экземпляр. Автоматически генерирует уникальный `slug` из поля `name`.
116
200
 
117
201
  ```typescript
118
202
  const instance = await sdk.createInstance(entityDefinitionId, {
119
- data: Record<string, unknown>;
203
+ data: Record<string, unknown>; // Обязательно должно содержать поле 'name' (string)
120
204
  relations?: Record<string, string[]>;
121
205
  });
122
206
  ```
123
207
 
208
+ **Особенности:**
209
+ - Автоматически генерирует уникальный `slug` из поля `name`
210
+ - Если slug уже существует, добавляет случайный суффикс
211
+ - Автоматически устанавливает `created_by` из текущего пользователя
212
+ - Поддерживает создание relations в одном запросе
213
+
124
214
  #### `updateInstance(entityDefinitionId, id, data)`
125
215
 
126
216
  Обновить экземпляр.
@@ -159,6 +249,16 @@ const config = await sdk.getEntityDefinitionWithUIConfig(entityDefinitionId);
159
249
  // config: { entityDefinition, fields, uiConfig }
160
250
  ```
161
251
 
252
+ #### `getAllEntityDefinitions()`
253
+
254
+ Получить все EntityDefinitions проекта с полями одним запросом (JOIN). Используется для загрузки всех сущностей в layout.
255
+
256
+ ```typescript
257
+ const entityDefinitions = await sdk.getAllEntityDefinitions();
258
+ // Возвращает массив EntityDefinitionConfig с полями
259
+ // Каждый элемент содержит: { id, name, slug, fields, ... }
260
+ ```
261
+
162
262
  ## 🔧 Типы
163
263
 
164
264
  ```typescript
@@ -173,6 +273,30 @@ import type {
173
273
  } from '@igorchugurov/public-api-sdk';
174
274
  ```
175
275
 
276
+ ### Slug поддержка
277
+
278
+ Все экземпляры и определения сущностей теперь содержат поле `slug`:
279
+
280
+ ```typescript
281
+ // EntityInstance содержит slug
282
+ const instance: EntityInstanceWithFields = {
283
+ id: string;
284
+ slug: string; // URL-friendly идентификатор
285
+ entityDefinitionId: string;
286
+ // ...
287
+ };
288
+
289
+ // EntityDefinition также содержит slug
290
+ const entityDef: EntityDefinition = {
291
+ id: string;
292
+ name: string;
293
+ slug: string; // URL-friendly идентификатор
294
+ // ...
295
+ };
296
+ ```
297
+
298
+ Slug автоматически генерируется при создании экземпляра из поля `name` и гарантирует уникальность в рамках одной EntityDefinition.
299
+
176
300
  ## 🛠️ Обработка ошибок
177
301
 
178
302
  SDK использует типизированные ошибки:
@@ -109,6 +109,7 @@ type PartialUIConfig = {
109
109
  interface EntityDefinition {
110
110
  id: string;
111
111
  name: string;
112
+ slug: string;
112
113
  description?: string | null;
113
114
  tableName: string;
114
115
  type: "primary" | "secondary" | "tertiary";
@@ -207,6 +208,7 @@ type DbTypeToTSType = {
207
208
  type EntityData<T extends Record<string, FieldValue> = Record<string, FieldValue>> = T;
208
209
  interface EntityInstance<TData extends Record<string, FieldValue> = Record<string, FieldValue>> {
209
210
  id: string;
211
+ slug: string;
210
212
  entityDefinitionId: string;
211
213
  projectId: string;
212
214
  data: EntityData<TData>;
@@ -230,6 +232,7 @@ interface EntityRelation {
230
232
  */
231
233
  type EntityInstanceWithFields<TFields extends Record<string, FieldValue> = Record<string, FieldValue>> = {
232
234
  id: string;
235
+ slug: string;
233
236
  entityDefinitionId: string;
234
237
  projectId: string;
235
238
  createdAt: string;
@@ -318,6 +321,7 @@ interface ProjectConfig {
318
321
  interface EntityDefinitionConfig {
319
322
  id: string;
320
323
  name: string;
324
+ slug: string;
321
325
  description?: string | null;
322
326
  tableName: string;
323
327
  type: "primary" | "secondary" | "tertiary";
@@ -1067,6 +1071,13 @@ declare abstract class BasePublicAPIClient {
1067
1071
  * @returns EntityDefinitionConfig с полями, отсортированными по display_index
1068
1072
  */
1069
1073
  getEntityDefinitionConfig(entityDefinitionId: string): Promise<EntityDefinitionConfig>;
1074
+ /**
1075
+ * Получить все EntityDefinitions проекта с полями одним запросом (JOIN)
1076
+ * Используется для загрузки всех сущностей в layout
1077
+ *
1078
+ * @returns Массив EntityDefinitionConfig с полями
1079
+ */
1080
+ getAllEntityDefinitions(): Promise<EntityDefinitionConfig[]>;
1070
1081
  /**
1071
1082
  * Преобразование EntityDefinitionConfig в EntityDefinition
1072
1083
  */
@@ -1098,6 +1109,9 @@ declare abstract class BasePublicAPIClient {
1098
1109
  abstract getInstance(entityDefinitionId: string, id: string, params?: {
1099
1110
  relationsAsIds?: boolean;
1100
1111
  }): Promise<EntityInstanceWithFields>;
1112
+ abstract getInstanceBySlug(entityDefinitionId: string, slug: string, params?: {
1113
+ relationsAsIds?: boolean;
1114
+ }): Promise<EntityInstanceWithFields>;
1101
1115
  abstract createInstance(entityDefinitionId: string, data: CreateInstanceData): Promise<EntityInstanceWithFields>;
1102
1116
  abstract updateInstance(entityDefinitionId: string, id: string, data: UpdateInstanceData): Promise<EntityInstanceWithFields>;
1103
1117
  abstract deleteInstance(entityDefinitionId: string, id: string): Promise<void>;
@@ -1141,6 +1155,12 @@ declare class PublicAPIClient extends BasePublicAPIClient {
1141
1155
  getInstance(entityDefinitionId: string, id: string, params?: {
1142
1156
  relationsAsIds?: boolean;
1143
1157
  }): Promise<EntityInstanceWithFields>;
1158
+ /**
1159
+ * Получить один экземпляр по slug
1160
+ */
1161
+ getInstanceBySlug(entityDefinitionId: string, slug: string, params?: {
1162
+ relationsAsIds?: boolean;
1163
+ }): Promise<EntityInstanceWithFields>;
1144
1164
  /**
1145
1165
  * Получить список экземпляров
1146
1166
  * Поддерживает поиск, фильтры (JSONB и relation), пагинацию
@@ -109,6 +109,7 @@ type PartialUIConfig = {
109
109
  interface EntityDefinition {
110
110
  id: string;
111
111
  name: string;
112
+ slug: string;
112
113
  description?: string | null;
113
114
  tableName: string;
114
115
  type: "primary" | "secondary" | "tertiary";
@@ -207,6 +208,7 @@ type DbTypeToTSType = {
207
208
  type EntityData<T extends Record<string, FieldValue> = Record<string, FieldValue>> = T;
208
209
  interface EntityInstance<TData extends Record<string, FieldValue> = Record<string, FieldValue>> {
209
210
  id: string;
211
+ slug: string;
210
212
  entityDefinitionId: string;
211
213
  projectId: string;
212
214
  data: EntityData<TData>;
@@ -230,6 +232,7 @@ interface EntityRelation {
230
232
  */
231
233
  type EntityInstanceWithFields<TFields extends Record<string, FieldValue> = Record<string, FieldValue>> = {
232
234
  id: string;
235
+ slug: string;
233
236
  entityDefinitionId: string;
234
237
  projectId: string;
235
238
  createdAt: string;
@@ -318,6 +321,7 @@ interface ProjectConfig {
318
321
  interface EntityDefinitionConfig {
319
322
  id: string;
320
323
  name: string;
324
+ slug: string;
321
325
  description?: string | null;
322
326
  tableName: string;
323
327
  type: "primary" | "secondary" | "tertiary";
@@ -1067,6 +1071,13 @@ declare abstract class BasePublicAPIClient {
1067
1071
  * @returns EntityDefinitionConfig с полями, отсортированными по display_index
1068
1072
  */
1069
1073
  getEntityDefinitionConfig(entityDefinitionId: string): Promise<EntityDefinitionConfig>;
1074
+ /**
1075
+ * Получить все EntityDefinitions проекта с полями одним запросом (JOIN)
1076
+ * Используется для загрузки всех сущностей в layout
1077
+ *
1078
+ * @returns Массив EntityDefinitionConfig с полями
1079
+ */
1080
+ getAllEntityDefinitions(): Promise<EntityDefinitionConfig[]>;
1070
1081
  /**
1071
1082
  * Преобразование EntityDefinitionConfig в EntityDefinition
1072
1083
  */
@@ -1098,6 +1109,9 @@ declare abstract class BasePublicAPIClient {
1098
1109
  abstract getInstance(entityDefinitionId: string, id: string, params?: {
1099
1110
  relationsAsIds?: boolean;
1100
1111
  }): Promise<EntityInstanceWithFields>;
1112
+ abstract getInstanceBySlug(entityDefinitionId: string, slug: string, params?: {
1113
+ relationsAsIds?: boolean;
1114
+ }): Promise<EntityInstanceWithFields>;
1101
1115
  abstract createInstance(entityDefinitionId: string, data: CreateInstanceData): Promise<EntityInstanceWithFields>;
1102
1116
  abstract updateInstance(entityDefinitionId: string, id: string, data: UpdateInstanceData): Promise<EntityInstanceWithFields>;
1103
1117
  abstract deleteInstance(entityDefinitionId: string, id: string): Promise<void>;
@@ -1141,6 +1155,12 @@ declare class PublicAPIClient extends BasePublicAPIClient {
1141
1155
  getInstance(entityDefinitionId: string, id: string, params?: {
1142
1156
  relationsAsIds?: boolean;
1143
1157
  }): Promise<EntityInstanceWithFields>;
1158
+ /**
1159
+ * Получить один экземпляр по slug
1160
+ */
1161
+ getInstanceBySlug(entityDefinitionId: string, slug: string, params?: {
1162
+ relationsAsIds?: boolean;
1163
+ }): Promise<EntityInstanceWithFields>;
1144
1164
  /**
1145
1165
  * Получить список экземпляров
1146
1166
  * Поддерживает поиск, фильтры (JSONB и relation), пагинацию
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FieldConfig, a as FieldValue, E as EntityDefinition, b as Field, c as EntityUIConfig, C as ColumnConfig, S as SDKOptions, P as PublicAPIClient } from './client-DqqjGYgA.mjs';
2
- export { y as ActionConfig, A as AuthResult, g as CreateInstanceData, D as DbType, l as DbTypeToTSType, m as EntityData, e as EntityDefinitionConfig, x as EntityFile, n as EntityInstance, q as EntityInstanceWithFields, p as EntityRelation, k as FieldOption, j as FieldType, r as FilterValue, z as FormPageConfig, G as GetInstancesOptions, I as InstanceData, L as ListPageConfig, M as MessagesConfig, h as PaginationResult, w as PartialInstanceData, B as PartialUIConfig, d as ProjectConfig, Q as QueryParams, R as RelationFilterInfo, f as RelationFilterMode, o as RelationType, s as RelationsData, i as SignUpData, U as UpdateInstanceData, v as getFieldValue, u as isEntityData, t as isFieldValue } from './client-DqqjGYgA.mjs';
1
+ import { F as FieldConfig, a as FieldValue, E as EntityDefinition, b as Field, c as EntityUIConfig, C as ColumnConfig, S as SDKOptions, P as PublicAPIClient } from './client-DS5xnLAo.mjs';
2
+ export { y as ActionConfig, A as AuthResult, g as CreateInstanceData, D as DbType, l as DbTypeToTSType, m as EntityData, e as EntityDefinitionConfig, x as EntityFile, n as EntityInstance, q as EntityInstanceWithFields, p as EntityRelation, k as FieldOption, j as FieldType, r as FilterValue, z as FormPageConfig, G as GetInstancesOptions, I as InstanceData, L as ListPageConfig, M as MessagesConfig, h as PaginationResult, w as PartialInstanceData, B as PartialUIConfig, d as ProjectConfig, Q as QueryParams, R as RelationFilterInfo, f as RelationFilterMode, o as RelationType, s as RelationsData, i as SignUpData, U as UpdateInstanceData, v as getFieldValue, u as isEntityData, t as isFieldValue } from './client-DS5xnLAo.mjs';
3
3
  import '@supabase/supabase-js';
4
4
 
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FieldConfig, a as FieldValue, E as EntityDefinition, b as Field, c as EntityUIConfig, C as ColumnConfig, S as SDKOptions, P as PublicAPIClient } from './client-DqqjGYgA.js';
2
- export { y as ActionConfig, A as AuthResult, g as CreateInstanceData, D as DbType, l as DbTypeToTSType, m as EntityData, e as EntityDefinitionConfig, x as EntityFile, n as EntityInstance, q as EntityInstanceWithFields, p as EntityRelation, k as FieldOption, j as FieldType, r as FilterValue, z as FormPageConfig, G as GetInstancesOptions, I as InstanceData, L as ListPageConfig, M as MessagesConfig, h as PaginationResult, w as PartialInstanceData, B as PartialUIConfig, d as ProjectConfig, Q as QueryParams, R as RelationFilterInfo, f as RelationFilterMode, o as RelationType, s as RelationsData, i as SignUpData, U as UpdateInstanceData, v as getFieldValue, u as isEntityData, t as isFieldValue } from './client-DqqjGYgA.js';
1
+ import { F as FieldConfig, a as FieldValue, E as EntityDefinition, b as Field, c as EntityUIConfig, C as ColumnConfig, S as SDKOptions, P as PublicAPIClient } from './client-DS5xnLAo.js';
2
+ export { y as ActionConfig, A as AuthResult, g as CreateInstanceData, D as DbType, l as DbTypeToTSType, m as EntityData, e as EntityDefinitionConfig, x as EntityFile, n as EntityInstance, q as EntityInstanceWithFields, p as EntityRelation, k as FieldOption, j as FieldType, r as FilterValue, z as FormPageConfig, G as GetInstancesOptions, I as InstanceData, L as ListPageConfig, M as MessagesConfig, h as PaginationResult, w as PartialInstanceData, B as PartialUIConfig, d as ProjectConfig, Q as QueryParams, R as RelationFilterInfo, f as RelationFilterMode, o as RelationType, s as RelationsData, i as SignUpData, U as UpdateInstanceData, v as getFieldValue, u as isEntityData, t as isFieldValue } from './client-DS5xnLAo.js';
3
3
  import '@supabase/supabase-js';
4
4
 
5
5
  /**
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ var ssr = require('@supabase/ssr');
5
5
  var __defProp = Object.defineProperty;
6
6
  var __defProps = Object.defineProperties;
7
7
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
9
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
10
  var __hasOwnProp = Object.prototype.hasOwnProperty;
10
11
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -21,6 +22,61 @@ var __spreadValues = (a, b) => {
21
22
  return a;
22
23
  };
23
24
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __esm = (fn, res) => function __init() {
26
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
27
+ };
28
+ var __export = (target, all) => {
29
+ for (var name in all)
30
+ __defProp(target, name, { get: all[name], enumerable: true });
31
+ };
32
+
33
+ // src/utils/slug.ts
34
+ var slug_exports = {};
35
+ __export(slug_exports, {
36
+ generateSlug: () => generateSlug,
37
+ generateUniqueSlugForInstance: () => generateUniqueSlugForInstance,
38
+ validateSlug: () => validateSlug
39
+ });
40
+ function generateSlug(name) {
41
+ if (!name || typeof name !== "string") {
42
+ throw new Error("Name must be a non-empty string");
43
+ }
44
+ return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 100);
45
+ }
46
+ function validateSlug(slug) {
47
+ if (!slug || typeof slug !== "string") {
48
+ return false;
49
+ }
50
+ if (slug.length === 0 || slug.length > 100) {
51
+ return false;
52
+ }
53
+ const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
54
+ return slugRegex.test(slug);
55
+ }
56
+ function generateRandomSuffix() {
57
+ return Math.random().toString(36).substring(2, 6);
58
+ }
59
+ async function generateUniqueSlugForInstance(name, entityDefinitionId, checkExists, excludeId) {
60
+ const baseSlug = generateSlug(name);
61
+ let slug = baseSlug;
62
+ let attempts = 0;
63
+ const maxAttempts = 100;
64
+ while (attempts < maxAttempts) {
65
+ const exists = await checkExists(slug, entityDefinitionId, excludeId);
66
+ if (!exists) {
67
+ return slug;
68
+ }
69
+ const randomSuffix = generateRandomSuffix();
70
+ slug = `${baseSlug}-${randomSuffix}`;
71
+ attempts++;
72
+ }
73
+ const timestamp = Date.now().toString(36);
74
+ return `${baseSlug}-${timestamp}`;
75
+ }
76
+ var init_slug = __esm({
77
+ "src/utils/slug.ts"() {
78
+ }
79
+ });
24
80
 
25
81
  // src/types/entity-types.ts
26
82
  function isFieldValue(value) {
@@ -397,6 +453,7 @@ var BasePublicAPIClient = class {
397
453
  return {
398
454
  id: row.id,
399
455
  name: row.name,
456
+ slug: row.slug,
400
457
  description: row.description,
401
458
  tableName: row.table_name,
402
459
  type: row.type,
@@ -519,6 +576,38 @@ var BasePublicAPIClient = class {
519
576
  }
520
577
  return config;
521
578
  }
579
+ /**
580
+ * Получить все EntityDefinitions проекта с полями одним запросом (JOIN)
581
+ * Используется для загрузки всех сущностей в layout
582
+ *
583
+ * @returns Массив EntityDefinitionConfig с полями
584
+ */
585
+ async getAllEntityDefinitions() {
586
+ const { data, error } = await this.supabase.from("entity_definition").select(
587
+ `
588
+ *,
589
+ field!field_entity_definition_id_fkey (*)
590
+ `
591
+ ).eq("project_id", this.projectId).order("name");
592
+ if (error) {
593
+ throw new Error(`Failed to load entity definitions: ${error.message}`);
594
+ }
595
+ if (!data || data.length === 0) {
596
+ return [];
597
+ }
598
+ return data.map((row) => {
599
+ const entityDefinition = this.transformEntityDefinitionFromDB(row);
600
+ const fields = (row.field || []).map(
601
+ (fieldRow) => this.transformFieldFromDB(fieldRow)
602
+ );
603
+ fields.sort(
604
+ (a, b) => a.displayIndex - b.displayIndex
605
+ );
606
+ return __spreadProps(__spreadValues({}, entityDefinition), {
607
+ fields
608
+ });
609
+ });
610
+ }
522
611
  /**
523
612
  * Преобразование EntityDefinitionConfig в EntityDefinition
524
613
  */
@@ -526,6 +615,7 @@ var BasePublicAPIClient = class {
526
615
  return {
527
616
  id: config.id,
528
617
  name: config.name,
618
+ slug: config.slug,
529
619
  description: config.description,
530
620
  tableName: config.tableName,
531
621
  type: config.type,
@@ -637,6 +727,7 @@ var BasePublicAPIClient = class {
637
727
  function transformEntityInstance(row) {
638
728
  return {
639
729
  id: row.id,
730
+ slug: row.slug,
640
731
  entityDefinitionId: row.entity_definition_id,
641
732
  projectId: row.project_id,
642
733
  data: row.data || {},
@@ -647,6 +738,7 @@ function transformEntityInstance(row) {
647
738
  function flattenInstance(instance, fields, relationsAsIds = false) {
648
739
  const result = {
649
740
  id: instance.id,
741
+ slug: instance.slug,
650
742
  entityDefinitionId: instance.entityDefinitionId,
651
743
  projectId: instance.projectId,
652
744
  createdAt: instance.createdAt,
@@ -675,6 +767,7 @@ function flattenInstance(instance, fields, relationsAsIds = false) {
675
767
  }
676
768
 
677
769
  // src/client.ts
770
+ init_slug();
678
771
  var _PublicAPIClient = class _PublicAPIClient extends BasePublicAPIClient {
679
772
  constructor(supabase, projectId, mode, options = {}) {
680
773
  super(supabase, projectId, options);
@@ -826,6 +919,134 @@ var _PublicAPIClient = class _PublicAPIClient extends BasePublicAPIClient {
826
919
  handleSupabaseError(error);
827
920
  }
828
921
  }
922
+ /**
923
+ * Получить один экземпляр по slug
924
+ */
925
+ async getInstanceBySlug(entityDefinitionId, slug, params) {
926
+ var _a;
927
+ try {
928
+ if (!validateSlug(slug)) {
929
+ throw new ValidationError(
930
+ "slug",
931
+ "Slug must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen"
932
+ );
933
+ }
934
+ const fields = await this.getFields(entityDefinitionId);
935
+ const { data: instance, error: instanceError } = await this.supabase.from("entity_instance").select("*").eq("slug", slug).eq("entity_definition_id", entityDefinitionId).eq("project_id", this.projectId).single();
936
+ if (instanceError || !instance) {
937
+ handleInstanceError(
938
+ instanceError || new Error("Instance not found"),
939
+ slug
940
+ );
941
+ }
942
+ const transformedInstance = transformEntityInstance(instance);
943
+ if (transformedInstance.entityDefinitionId !== entityDefinitionId) {
944
+ throw new NotFoundError("Entity instance", slug);
945
+ }
946
+ const relationFields = fields.filter(
947
+ (f) => f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne"
948
+ );
949
+ const relations = {};
950
+ if (relationFields.length > 0) {
951
+ const relationFieldIds = relationFields.map((f) => f.id).filter((id) => Boolean(id));
952
+ if (relationFieldIds.length > 0) {
953
+ const { data: allRelations, error: relationsError } = await this.supabase.from("entity_relation").select("target_instance_id, relation_field_id").eq("source_instance_id", transformedInstance.id).in("relation_field_id", relationFieldIds);
954
+ if (relationsError) {
955
+ throw new SDKError(
956
+ "RELATIONS_LOAD_ERROR",
957
+ `Failed to load relations for instance with slug ${slug}: ${relationsError.message}`,
958
+ 500,
959
+ relationsError
960
+ );
961
+ }
962
+ if (allRelations && allRelations.length > 0) {
963
+ const targetInstanceIds = [
964
+ ...new Set(allRelations.map((r) => r.target_instance_id))
965
+ ];
966
+ const { data: relatedInstances, error: instancesError } = await this.supabase.from("entity_instance").select("*").in("id", targetInstanceIds);
967
+ if (instancesError) {
968
+ throw new SDKError(
969
+ "RELATED_INSTANCES_LOAD_ERROR",
970
+ `Failed to load related instances for instance with slug ${slug}: ${instancesError.message}`,
971
+ 500,
972
+ instancesError
973
+ );
974
+ }
975
+ if (relatedInstances) {
976
+ const relatedInstancesMap = new Map(
977
+ relatedInstances.map((inst) => [
978
+ inst.id,
979
+ transformEntityInstance(inst)
980
+ ])
981
+ );
982
+ for (const relation of allRelations) {
983
+ const relationField = relationFields.find(
984
+ (f) => f.id === relation.relation_field_id
985
+ );
986
+ if (relationField) {
987
+ const relatedInstance = relatedInstancesMap.get(
988
+ relation.target_instance_id
989
+ );
990
+ if (relatedInstance) {
991
+ if (!relations[relationField.name]) {
992
+ relations[relationField.name] = [];
993
+ }
994
+ relations[relationField.name].push(
995
+ relatedInstance
996
+ );
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+ const fileFields = fields.filter(
1005
+ (f) => f.type === "files" || f.type === "images"
1006
+ );
1007
+ if (fileFields.length > 0) {
1008
+ const { data: allFiles, error: filesError } = await this.supabase.from("entity_file").select("id, field_id").eq("entity_instance_id", transformedInstance.id);
1009
+ if (filesError) {
1010
+ throw new SDKError(
1011
+ "FILES_LOAD_ERROR",
1012
+ `Failed to load files for instance with slug ${slug}: ${filesError.message}`,
1013
+ 500,
1014
+ filesError
1015
+ );
1016
+ }
1017
+ if (allFiles) {
1018
+ const filesByFieldId = /* @__PURE__ */ new Map();
1019
+ allFiles.forEach((file) => {
1020
+ if (file.field_id) {
1021
+ if (!filesByFieldId.has(file.field_id)) {
1022
+ filesByFieldId.set(file.field_id, []);
1023
+ }
1024
+ filesByFieldId.get(file.field_id).push(file.id);
1025
+ }
1026
+ });
1027
+ fileFields.forEach((field) => {
1028
+ const fileIds = filesByFieldId.get(field.id) || [];
1029
+ if (fileIds.length > 0 || !transformedInstance.data[field.name]) {
1030
+ transformedInstance.data[field.name] = fileIds;
1031
+ }
1032
+ });
1033
+ }
1034
+ }
1035
+ const instanceWithRelations = __spreadProps(__spreadValues({}, transformedInstance), {
1036
+ relations: Object.keys(relations).length > 0 ? relations : void 0
1037
+ });
1038
+ return flattenInstance(
1039
+ instanceWithRelations,
1040
+ fields.map((f) => ({ name: f.name, dbType: f.dbType })),
1041
+ (_a = params == null ? void 0 : params.relationsAsIds) != null ? _a : false
1042
+ );
1043
+ } catch (error) {
1044
+ if (error instanceof NotFoundError || error instanceof PermissionDeniedError || error instanceof ValidationError || error instanceof SDKError) {
1045
+ throw error;
1046
+ }
1047
+ handleSupabaseError(error);
1048
+ }
1049
+ }
829
1050
  /**
830
1051
  * Получить список экземпляров
831
1052
  * Поддерживает поиск, фильтры (JSONB и relation), пагинацию
@@ -1214,9 +1435,27 @@ var _PublicAPIClient = class _PublicAPIClient extends BasePublicAPIClient {
1214
1435
  );
1215
1436
  }
1216
1437
  const { data: instanceData, relations } = data;
1438
+ const name = instanceData.name;
1439
+ if (!name || typeof name !== "string") {
1440
+ throw new Error(
1441
+ "Field 'name' is required and must be a string for slug generation"
1442
+ );
1443
+ }
1444
+ const {
1445
+ generateUniqueSlugForInstance: generateUniqueSlugForInstance2
1446
+ } = await Promise.resolve().then(() => (init_slug(), slug_exports));
1447
+ const slug = await generateUniqueSlugForInstance2(
1448
+ name,
1449
+ entityDefinitionId,
1450
+ async (slugToCheck, entityDefIdToCheck) => {
1451
+ const { data: existing } = await this.supabase.from("entity_instance").select("id").eq("entity_definition_id", entityDefIdToCheck).eq("slug", slugToCheck).single();
1452
+ return !!existing;
1453
+ }
1454
+ );
1217
1455
  const { data: instance, error: instanceError } = await this.supabase.from("entity_instance").insert({
1218
1456
  entity_definition_id: entityDefinitionId,
1219
1457
  project_id: this.projectId,
1458
+ slug,
1220
1459
  data: instanceData,
1221
1460
  created_by: (user == null ? void 0 : user.id) || null
1222
1461
  }).select().single();