@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/dist/server.mjs CHANGED
@@ -3,6 +3,7 @@ import { createServerClient as createServerClient$1 } from '@supabase/ssr';
3
3
  var __defProp = Object.defineProperty;
4
4
  var __defProps = Object.defineProperties;
5
5
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
7
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
9
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -19,6 +20,61 @@ var __spreadValues = (a, b) => {
19
20
  return a;
20
21
  };
21
22
  var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
23
+ var __esm = (fn, res) => function __init() {
24
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
25
+ };
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+
31
+ // src/utils/slug.ts
32
+ var slug_exports = {};
33
+ __export(slug_exports, {
34
+ generateSlug: () => generateSlug,
35
+ generateUniqueSlugForInstance: () => generateUniqueSlugForInstance,
36
+ validateSlug: () => validateSlug
37
+ });
38
+ function generateSlug(name) {
39
+ if (!name || typeof name !== "string") {
40
+ throw new Error("Name must be a non-empty string");
41
+ }
42
+ return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 100);
43
+ }
44
+ function validateSlug(slug) {
45
+ if (!slug || typeof slug !== "string") {
46
+ return false;
47
+ }
48
+ if (slug.length === 0 || slug.length > 100) {
49
+ return false;
50
+ }
51
+ const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
52
+ return slugRegex.test(slug);
53
+ }
54
+ function generateRandomSuffix() {
55
+ return Math.random().toString(36).substring(2, 6);
56
+ }
57
+ async function generateUniqueSlugForInstance(name, entityDefinitionId, checkExists, excludeId) {
58
+ const baseSlug = generateSlug(name);
59
+ let slug = baseSlug;
60
+ let attempts = 0;
61
+ const maxAttempts = 100;
62
+ while (attempts < maxAttempts) {
63
+ const exists = await checkExists(slug, entityDefinitionId, excludeId);
64
+ if (!exists) {
65
+ return slug;
66
+ }
67
+ const randomSuffix = generateRandomSuffix();
68
+ slug = `${baseSlug}-${randomSuffix}`;
69
+ attempts++;
70
+ }
71
+ const timestamp = Date.now().toString(36);
72
+ return `${baseSlug}-${timestamp}`;
73
+ }
74
+ var init_slug = __esm({
75
+ "src/utils/slug.ts"() {
76
+ }
77
+ });
22
78
  function createServerClient(supabaseUrl, supabaseAnonKey, options) {
23
79
  return createServerClient$1(supabaseUrl, supabaseAnonKey, {
24
80
  cookies: (options == null ? void 0 : options.cookies) || {
@@ -224,6 +280,7 @@ var BasePublicAPIClient = class {
224
280
  return {
225
281
  id: row.id,
226
282
  name: row.name,
283
+ slug: row.slug,
227
284
  description: row.description,
228
285
  tableName: row.table_name,
229
286
  type: row.type,
@@ -346,6 +403,38 @@ var BasePublicAPIClient = class {
346
403
  }
347
404
  return config;
348
405
  }
406
+ /**
407
+ * Получить все EntityDefinitions проекта с полями одним запросом (JOIN)
408
+ * Используется для загрузки всех сущностей в layout
409
+ *
410
+ * @returns Массив EntityDefinitionConfig с полями
411
+ */
412
+ async getAllEntityDefinitions() {
413
+ const { data, error } = await this.supabase.from("entity_definition").select(
414
+ `
415
+ *,
416
+ field!field_entity_definition_id_fkey (*)
417
+ `
418
+ ).eq("project_id", this.projectId).order("name");
419
+ if (error) {
420
+ throw new Error(`Failed to load entity definitions: ${error.message}`);
421
+ }
422
+ if (!data || data.length === 0) {
423
+ return [];
424
+ }
425
+ return data.map((row) => {
426
+ const entityDefinition = this.transformEntityDefinitionFromDB(row);
427
+ const fields = (row.field || []).map(
428
+ (fieldRow) => this.transformFieldFromDB(fieldRow)
429
+ );
430
+ fields.sort(
431
+ (a, b) => a.displayIndex - b.displayIndex
432
+ );
433
+ return __spreadProps(__spreadValues({}, entityDefinition), {
434
+ fields
435
+ });
436
+ });
437
+ }
349
438
  /**
350
439
  * Преобразование EntityDefinitionConfig в EntityDefinition
351
440
  */
@@ -353,6 +442,7 @@ var BasePublicAPIClient = class {
353
442
  return {
354
443
  id: config.id,
355
444
  name: config.name,
445
+ slug: config.slug,
356
446
  description: config.description,
357
447
  tableName: config.tableName,
358
448
  type: config.type,
@@ -464,6 +554,7 @@ var BasePublicAPIClient = class {
464
554
  function transformEntityInstance(row) {
465
555
  return {
466
556
  id: row.id,
557
+ slug: row.slug,
467
558
  entityDefinitionId: row.entity_definition_id,
468
559
  projectId: row.project_id,
469
560
  data: row.data || {},
@@ -474,6 +565,7 @@ function transformEntityInstance(row) {
474
565
  function flattenInstance(instance, fields, relationsAsIds = false) {
475
566
  const result = {
476
567
  id: instance.id,
568
+ slug: instance.slug,
477
569
  entityDefinitionId: instance.entityDefinitionId,
478
570
  projectId: instance.projectId,
479
571
  createdAt: instance.createdAt,
@@ -501,6 +593,9 @@ function flattenInstance(instance, fields, relationsAsIds = false) {
501
593
  return result;
502
594
  }
503
595
 
596
+ // src/client.ts
597
+ init_slug();
598
+
504
599
  // src/errors.ts
505
600
  var SDKError = class extends Error {
506
601
  constructor(code, message, statusCode, details) {
@@ -529,6 +624,19 @@ var PermissionDeniedError = class extends SDKError {
529
624
  );
530
625
  }
531
626
  };
627
+ var ValidationError = class extends SDKError {
628
+ constructor(field, message) {
629
+ super(
630
+ "VALIDATION_ERROR",
631
+ `Validation failed for ${field}: ${message}`,
632
+ 400,
633
+ {
634
+ field,
635
+ message
636
+ }
637
+ );
638
+ }
639
+ };
532
640
  function handleSupabaseError(error) {
533
641
  if (error.code === "PGRST116") {
534
642
  throw new NotFoundError("Resource");
@@ -711,6 +819,134 @@ var _PublicAPIClient = class _PublicAPIClient extends BasePublicAPIClient {
711
819
  handleSupabaseError(error);
712
820
  }
713
821
  }
822
+ /**
823
+ * Получить один экземпляр по slug
824
+ */
825
+ async getInstanceBySlug(entityDefinitionId, slug, params) {
826
+ var _a;
827
+ try {
828
+ if (!validateSlug(slug)) {
829
+ throw new ValidationError(
830
+ "slug",
831
+ "Slug must contain only lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen"
832
+ );
833
+ }
834
+ const fields = await this.getFields(entityDefinitionId);
835
+ 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();
836
+ if (instanceError || !instance) {
837
+ handleInstanceError(
838
+ instanceError || new Error("Instance not found"),
839
+ slug
840
+ );
841
+ }
842
+ const transformedInstance = transformEntityInstance(instance);
843
+ if (transformedInstance.entityDefinitionId !== entityDefinitionId) {
844
+ throw new NotFoundError("Entity instance", slug);
845
+ }
846
+ const relationFields = fields.filter(
847
+ (f) => f.dbType === "manyToMany" || f.dbType === "manyToOne" || f.dbType === "oneToMany" || f.dbType === "oneToOne"
848
+ );
849
+ const relations = {};
850
+ if (relationFields.length > 0) {
851
+ const relationFieldIds = relationFields.map((f) => f.id).filter((id) => Boolean(id));
852
+ if (relationFieldIds.length > 0) {
853
+ 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);
854
+ if (relationsError) {
855
+ throw new SDKError(
856
+ "RELATIONS_LOAD_ERROR",
857
+ `Failed to load relations for instance with slug ${slug}: ${relationsError.message}`,
858
+ 500,
859
+ relationsError
860
+ );
861
+ }
862
+ if (allRelations && allRelations.length > 0) {
863
+ const targetInstanceIds = [
864
+ ...new Set(allRelations.map((r) => r.target_instance_id))
865
+ ];
866
+ const { data: relatedInstances, error: instancesError } = await this.supabase.from("entity_instance").select("*").in("id", targetInstanceIds);
867
+ if (instancesError) {
868
+ throw new SDKError(
869
+ "RELATED_INSTANCES_LOAD_ERROR",
870
+ `Failed to load related instances for instance with slug ${slug}: ${instancesError.message}`,
871
+ 500,
872
+ instancesError
873
+ );
874
+ }
875
+ if (relatedInstances) {
876
+ const relatedInstancesMap = new Map(
877
+ relatedInstances.map((inst) => [
878
+ inst.id,
879
+ transformEntityInstance(inst)
880
+ ])
881
+ );
882
+ for (const relation of allRelations) {
883
+ const relationField = relationFields.find(
884
+ (f) => f.id === relation.relation_field_id
885
+ );
886
+ if (relationField) {
887
+ const relatedInstance = relatedInstancesMap.get(
888
+ relation.target_instance_id
889
+ );
890
+ if (relatedInstance) {
891
+ if (!relations[relationField.name]) {
892
+ relations[relationField.name] = [];
893
+ }
894
+ relations[relationField.name].push(
895
+ relatedInstance
896
+ );
897
+ }
898
+ }
899
+ }
900
+ }
901
+ }
902
+ }
903
+ }
904
+ const fileFields = fields.filter(
905
+ (f) => f.type === "files" || f.type === "images"
906
+ );
907
+ if (fileFields.length > 0) {
908
+ const { data: allFiles, error: filesError } = await this.supabase.from("entity_file").select("id, field_id").eq("entity_instance_id", transformedInstance.id);
909
+ if (filesError) {
910
+ throw new SDKError(
911
+ "FILES_LOAD_ERROR",
912
+ `Failed to load files for instance with slug ${slug}: ${filesError.message}`,
913
+ 500,
914
+ filesError
915
+ );
916
+ }
917
+ if (allFiles) {
918
+ const filesByFieldId = /* @__PURE__ */ new Map();
919
+ allFiles.forEach((file) => {
920
+ if (file.field_id) {
921
+ if (!filesByFieldId.has(file.field_id)) {
922
+ filesByFieldId.set(file.field_id, []);
923
+ }
924
+ filesByFieldId.get(file.field_id).push(file.id);
925
+ }
926
+ });
927
+ fileFields.forEach((field) => {
928
+ const fileIds = filesByFieldId.get(field.id) || [];
929
+ if (fileIds.length > 0 || !transformedInstance.data[field.name]) {
930
+ transformedInstance.data[field.name] = fileIds;
931
+ }
932
+ });
933
+ }
934
+ }
935
+ const instanceWithRelations = __spreadProps(__spreadValues({}, transformedInstance), {
936
+ relations: Object.keys(relations).length > 0 ? relations : void 0
937
+ });
938
+ return flattenInstance(
939
+ instanceWithRelations,
940
+ fields.map((f) => ({ name: f.name, dbType: f.dbType })),
941
+ (_a = params == null ? void 0 : params.relationsAsIds) != null ? _a : false
942
+ );
943
+ } catch (error) {
944
+ if (error instanceof NotFoundError || error instanceof PermissionDeniedError || error instanceof ValidationError || error instanceof SDKError) {
945
+ throw error;
946
+ }
947
+ handleSupabaseError(error);
948
+ }
949
+ }
714
950
  /**
715
951
  * Получить список экземпляров
716
952
  * Поддерживает поиск, фильтры (JSONB и relation), пагинацию
@@ -1099,9 +1335,27 @@ var _PublicAPIClient = class _PublicAPIClient extends BasePublicAPIClient {
1099
1335
  );
1100
1336
  }
1101
1337
  const { data: instanceData, relations } = data;
1338
+ const name = instanceData.name;
1339
+ if (!name || typeof name !== "string") {
1340
+ throw new Error(
1341
+ "Field 'name' is required and must be a string for slug generation"
1342
+ );
1343
+ }
1344
+ const {
1345
+ generateUniqueSlugForInstance: generateUniqueSlugForInstance2
1346
+ } = await Promise.resolve().then(() => (init_slug(), slug_exports));
1347
+ const slug = await generateUniqueSlugForInstance2(
1348
+ name,
1349
+ entityDefinitionId,
1350
+ async (slugToCheck, entityDefIdToCheck) => {
1351
+ const { data: existing } = await this.supabase.from("entity_instance").select("id").eq("entity_definition_id", entityDefIdToCheck).eq("slug", slugToCheck).single();
1352
+ return !!existing;
1353
+ }
1354
+ );
1102
1355
  const { data: instance, error: instanceError } = await this.supabase.from("entity_instance").insert({
1103
1356
  entity_definition_id: entityDefinitionId,
1104
1357
  project_id: this.projectId,
1358
+ slug,
1105
1359
  data: instanceData,
1106
1360
  created_by: (user == null ? void 0 : user.id) || null
1107
1361
  }).select().single();