@igorchugurov/public-api-sdk 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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)
@@ -112,15 +162,21 @@ const instance = await sdk.getInstance(entityDefinitionId, id, {
112
162
 
113
163
  #### `createInstance(entityDefinitionId, data)`
114
164
 
115
- Создать новый экземпляр.
165
+ Создать новый экземпляр. Автоматически генерирует уникальный `slug` из поля `name`.
116
166
 
117
167
  ```typescript
118
168
  const instance = await sdk.createInstance(entityDefinitionId, {
119
- data: Record<string, unknown>;
169
+ data: Record<string, unknown>; // Обязательно должно содержать поле 'name' (string)
120
170
  relations?: Record<string, string[]>;
121
171
  });
122
172
  ```
123
173
 
174
+ **Особенности:**
175
+ - Автоматически генерирует уникальный `slug` из поля `name`
176
+ - Если slug уже существует, добавляет случайный суффикс
177
+ - Автоматически устанавливает `created_by` из текущего пользователя
178
+ - Поддерживает создание relations в одном запросе
179
+
124
180
  #### `updateInstance(entityDefinitionId, id, data)`
125
181
 
126
182
  Обновить экземпляр.
@@ -159,6 +215,16 @@ const config = await sdk.getEntityDefinitionWithUIConfig(entityDefinitionId);
159
215
  // config: { entityDefinition, fields, uiConfig }
160
216
  ```
161
217
 
218
+ #### `getAllEntityDefinitions()`
219
+
220
+ Получить все EntityDefinitions проекта с полями одним запросом (JOIN). Используется для загрузки всех сущностей в layout.
221
+
222
+ ```typescript
223
+ const entityDefinitions = await sdk.getAllEntityDefinitions();
224
+ // Возвращает массив EntityDefinitionConfig с полями
225
+ // Каждый элемент содержит: { id, name, slug, fields, ... }
226
+ ```
227
+
162
228
  ## 🔧 Типы
163
229
 
164
230
  ```typescript
@@ -173,6 +239,30 @@ import type {
173
239
  } from '@igorchugurov/public-api-sdk';
174
240
  ```
175
241
 
242
+ ### Slug поддержка
243
+
244
+ Все экземпляры и определения сущностей теперь содержат поле `slug`:
245
+
246
+ ```typescript
247
+ // EntityInstance содержит slug
248
+ const instance: EntityInstanceWithFields = {
249
+ id: string;
250
+ slug: string; // URL-friendly идентификатор
251
+ entityDefinitionId: string;
252
+ // ...
253
+ };
254
+
255
+ // EntityDefinition также содержит slug
256
+ const entityDef: EntityDefinition = {
257
+ id: string;
258
+ name: string;
259
+ slug: string; // URL-friendly идентификатор
260
+ // ...
261
+ };
262
+ ```
263
+
264
+ Slug автоматически генерируется при создании экземпляра из поля `name` и гарантирует уникальность в рамках одной EntityDefinition.
265
+
176
266
  ## 🛠️ Обработка ошибок
177
267
 
178
268
  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
  */
@@ -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
  */
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-BV5AAKYo.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-BV5AAKYo.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-BV5AAKYo.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-BV5AAKYo.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,50 @@ 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
+ });
39
+ function generateSlug(name) {
40
+ if (!name || typeof name !== "string") {
41
+ throw new Error("Name must be a non-empty string");
42
+ }
43
+ return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 100);
44
+ }
45
+ function generateRandomSuffix() {
46
+ return Math.random().toString(36).substring(2, 6);
47
+ }
48
+ async function generateUniqueSlugForInstance(name, entityDefinitionId, checkExists, excludeId) {
49
+ const baseSlug = generateSlug(name);
50
+ let slug = baseSlug;
51
+ let attempts = 0;
52
+ const maxAttempts = 100;
53
+ while (attempts < maxAttempts) {
54
+ const exists = await checkExists(slug, entityDefinitionId, excludeId);
55
+ if (!exists) {
56
+ return slug;
57
+ }
58
+ const randomSuffix = generateRandomSuffix();
59
+ slug = `${baseSlug}-${randomSuffix}`;
60
+ attempts++;
61
+ }
62
+ const timestamp = Date.now().toString(36);
63
+ return `${baseSlug}-${timestamp}`;
64
+ }
65
+ var init_slug = __esm({
66
+ "src/utils/slug.ts"() {
67
+ }
68
+ });
24
69
 
25
70
  // src/types/entity-types.ts
26
71
  function isFieldValue(value) {
@@ -397,6 +442,7 @@ var BasePublicAPIClient = class {
397
442
  return {
398
443
  id: row.id,
399
444
  name: row.name,
445
+ slug: row.slug,
400
446
  description: row.description,
401
447
  tableName: row.table_name,
402
448
  type: row.type,
@@ -519,6 +565,38 @@ var BasePublicAPIClient = class {
519
565
  }
520
566
  return config;
521
567
  }
568
+ /**
569
+ * Получить все EntityDefinitions проекта с полями одним запросом (JOIN)
570
+ * Используется для загрузки всех сущностей в layout
571
+ *
572
+ * @returns Массив EntityDefinitionConfig с полями
573
+ */
574
+ async getAllEntityDefinitions() {
575
+ const { data, error } = await this.supabase.from("entity_definition").select(
576
+ `
577
+ *,
578
+ field!field_entity_definition_id_fkey (*)
579
+ `
580
+ ).eq("project_id", this.projectId).order("name");
581
+ if (error) {
582
+ throw new Error(`Failed to load entity definitions: ${error.message}`);
583
+ }
584
+ if (!data || data.length === 0) {
585
+ return [];
586
+ }
587
+ return data.map((row) => {
588
+ const entityDefinition = this.transformEntityDefinitionFromDB(row);
589
+ const fields = (row.field || []).map(
590
+ (fieldRow) => this.transformFieldFromDB(fieldRow)
591
+ );
592
+ fields.sort(
593
+ (a, b) => a.displayIndex - b.displayIndex
594
+ );
595
+ return __spreadProps(__spreadValues({}, entityDefinition), {
596
+ fields
597
+ });
598
+ });
599
+ }
522
600
  /**
523
601
  * Преобразование EntityDefinitionConfig в EntityDefinition
524
602
  */
@@ -526,6 +604,7 @@ var BasePublicAPIClient = class {
526
604
  return {
527
605
  id: config.id,
528
606
  name: config.name,
607
+ slug: config.slug,
529
608
  description: config.description,
530
609
  tableName: config.tableName,
531
610
  type: config.type,
@@ -637,6 +716,7 @@ var BasePublicAPIClient = class {
637
716
  function transformEntityInstance(row) {
638
717
  return {
639
718
  id: row.id,
719
+ slug: row.slug,
640
720
  entityDefinitionId: row.entity_definition_id,
641
721
  projectId: row.project_id,
642
722
  data: row.data || {},
@@ -647,6 +727,7 @@ function transformEntityInstance(row) {
647
727
  function flattenInstance(instance, fields, relationsAsIds = false) {
648
728
  const result = {
649
729
  id: instance.id,
730
+ slug: instance.slug,
650
731
  entityDefinitionId: instance.entityDefinitionId,
651
732
  projectId: instance.projectId,
652
733
  createdAt: instance.createdAt,
@@ -1214,9 +1295,27 @@ var _PublicAPIClient = class _PublicAPIClient extends BasePublicAPIClient {
1214
1295
  );
1215
1296
  }
1216
1297
  const { data: instanceData, relations } = data;
1298
+ const name = instanceData.name;
1299
+ if (!name || typeof name !== "string") {
1300
+ throw new Error(
1301
+ "Field 'name' is required and must be a string for slug generation"
1302
+ );
1303
+ }
1304
+ const {
1305
+ generateUniqueSlugForInstance: generateUniqueSlugForInstance2
1306
+ } = await Promise.resolve().then(() => (init_slug(), slug_exports));
1307
+ const slug = await generateUniqueSlugForInstance2(
1308
+ name,
1309
+ entityDefinitionId,
1310
+ async (slugToCheck, entityDefIdToCheck) => {
1311
+ const { data: existing } = await this.supabase.from("entity_instance").select("id").eq("entity_definition_id", entityDefIdToCheck).eq("slug", slugToCheck).single();
1312
+ return !!existing;
1313
+ }
1314
+ );
1217
1315
  const { data: instance, error: instanceError } = await this.supabase.from("entity_instance").insert({
1218
1316
  entity_definition_id: entityDefinitionId,
1219
1317
  project_id: this.projectId,
1318
+ slug,
1220
1319
  data: instanceData,
1221
1320
  created_by: (user == null ? void 0 : user.id) || null
1222
1321
  }).select().single();