@bacano/sdk 1.1.10 → 1.1.12
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/CONTENT_TYPE_MANAGEMENT.md +192 -0
- package/README.md +19 -2
- package/content-management.cjs +135 -0
- package/content-management.d.ts +144 -0
- package/content-management.js +114 -0
- package/package.json +6 -1
- package/skills/bacano-figma-web/SKILL.md +6 -3
- package/skills/bacano-figma-web/references/content-workflow.md +21 -10
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Gestión de tipos de Content para agentes
|
|
2
|
+
|
|
3
|
+
`@bacano/sdk/content-management` permite que una herramienta administrativa o
|
|
4
|
+
un agente autorizado consulte, cree y mantenga **tipos y componentes** de
|
|
5
|
+
Bacano Content. No permite administrar entradas, productos, ventas, clientes ni
|
|
6
|
+
ningún otro dato de la empresa.
|
|
7
|
+
|
|
8
|
+
Este contrato es independiente del cliente público del storefront. Debe usarse
|
|
9
|
+
solo desde un proceso local o servidor confiable; nunca desde el navegador ni
|
|
10
|
+
dentro del bundle del sitio web.
|
|
11
|
+
|
|
12
|
+
## Activar un acceso temporal
|
|
13
|
+
|
|
14
|
+
1. En Bacano, abre **Sitios web → Contenido → Tipos** o **Componentes**.
|
|
15
|
+
2. Selecciona **Conectar agente**.
|
|
16
|
+
3. Elige 15, 30 o 60 minutos y genera el acceso.
|
|
17
|
+
4. Copia el token al entorno local del agente.
|
|
18
|
+
|
|
19
|
+
El token está firmado, pertenece a una sola empresa y solo contiene el alcance
|
|
20
|
+
`content_types:manage`. Cada petición vuelve a comprobar que el usuario que lo
|
|
21
|
+
generó continúa vinculado a la empresa y conserva `websites.manage`. Al vencer
|
|
22
|
+
o perder ese permiso deja de funcionar.
|
|
23
|
+
|
|
24
|
+
No guardes el token en Git, Figma, Content, logs, variables públicas ni archivos
|
|
25
|
+
que se empaqueten con el storefront.
|
|
26
|
+
|
|
27
|
+
## Crear el cliente
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createContentManagementClient } from "@bacano/sdk/content-management";
|
|
31
|
+
|
|
32
|
+
const content = createContentManagementClient({
|
|
33
|
+
apiUrl: process.env.BACANO_API_URL!,
|
|
34
|
+
accessToken: process.env.BACANO_CONTENT_ACCESS_TOKEN!,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const context = await content.getContext();
|
|
38
|
+
console.log(context.companyId, context.expiresAt);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
El cliente ofrece:
|
|
42
|
+
|
|
43
|
+
- `getContext()` para confirmar empresa, alcance y vencimiento.
|
|
44
|
+
- `getReferences()` para consultar categorías, subcategorías, listas de opciones
|
|
45
|
+
y unidades de medida válidas.
|
|
46
|
+
- `listTypes()` y `getType(idOrKey)`.
|
|
47
|
+
- `createType(input)` y `updateType(idOrKey, input)`.
|
|
48
|
+
- `inspectLifecycle(idOrKey, operation)`.
|
|
49
|
+
- `applyLifecycle(idOrKey, operation)`.
|
|
50
|
+
|
|
51
|
+
## Flujo seguro para un agente
|
|
52
|
+
|
|
53
|
+
1. Ejecutar `getContext()` y confirmar la empresa con el usuario.
|
|
54
|
+
2. Ejecutar `listTypes()` y `getReferences()` antes de proponer cambios.
|
|
55
|
+
3. Reutilizar tipos, componentes, listas y keys existentes cuando correspondan.
|
|
56
|
+
4. Crear primero los componentes que serán referenciados por otros tipos.
|
|
57
|
+
5. Crear o actualizar en `draft`.
|
|
58
|
+
6. Volver a consultar el tipo y presentar un resumen del resultado.
|
|
59
|
+
7. Activarlo únicamente cuando el usuario haya aprobado la estructura.
|
|
60
|
+
8. Para archivar, restaurar o eliminar, ejecutar primero
|
|
61
|
+
`inspectLifecycle()` y mostrar dependencias y bloqueos.
|
|
62
|
+
9. Ejecutar `applyLifecycle()` solo después de una confirmación explícita.
|
|
63
|
+
|
|
64
|
+
`updateType()` reemplaza la definición completa. Antes de usarlo debes obtener
|
|
65
|
+
el tipo con `getType()`, conservar los IDs de campos existentes y enviar todos
|
|
66
|
+
los campos y secciones que deban permanecer. La omisión de un campo se interpreta
|
|
67
|
+
como una eliminación estructural y puede requerir
|
|
68
|
+
`confirmDestructiveChanges: true`.
|
|
69
|
+
|
|
70
|
+
## Ejemplo: componente y tipo editorial
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
await content.createType({
|
|
74
|
+
key: "cta_button",
|
|
75
|
+
name: "Botón de llamada a la acción",
|
|
76
|
+
kind: "component",
|
|
77
|
+
status: "draft",
|
|
78
|
+
fields: [
|
|
79
|
+
{
|
|
80
|
+
key: "label",
|
|
81
|
+
label: "Texto",
|
|
82
|
+
type: "text",
|
|
83
|
+
required: true,
|
|
84
|
+
sortOrder: 1,
|
|
85
|
+
settings: { isVisible: true, maxLength: 80 },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "url",
|
|
89
|
+
label: "Enlace",
|
|
90
|
+
type: "link",
|
|
91
|
+
required: true,
|
|
92
|
+
sortOrder: 2,
|
|
93
|
+
settings: { isVisible: true },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await content.createType({
|
|
99
|
+
key: "home_hero",
|
|
100
|
+
name: "Hero del inicio",
|
|
101
|
+
kind: "entry",
|
|
102
|
+
status: "draft",
|
|
103
|
+
usages: ["website_content"],
|
|
104
|
+
sections: [{ key: "content", title: "Contenido", sortOrder: 1 }],
|
|
105
|
+
fields: [
|
|
106
|
+
{
|
|
107
|
+
key: "title",
|
|
108
|
+
label: "Título",
|
|
109
|
+
type: "text",
|
|
110
|
+
required: true,
|
|
111
|
+
sortOrder: 1,
|
|
112
|
+
sectionKey: "content",
|
|
113
|
+
settings: { isVisible: true, maxLength: 120 },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "actions",
|
|
117
|
+
label: "Acciones",
|
|
118
|
+
type: "repeater",
|
|
119
|
+
required: false,
|
|
120
|
+
sortOrder: 2,
|
|
121
|
+
sectionKey: "content",
|
|
122
|
+
settings: { isVisible: true, minRows: 0, maxRows: 3 },
|
|
123
|
+
children: [
|
|
124
|
+
{
|
|
125
|
+
key: "button",
|
|
126
|
+
label: "Botón",
|
|
127
|
+
type: "component",
|
|
128
|
+
required: true,
|
|
129
|
+
sortOrder: 1,
|
|
130
|
+
componentKey: "cta_button",
|
|
131
|
+
settings: { isVisible: true },
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Ajustes de campos
|
|
140
|
+
|
|
141
|
+
Todos los campos aceptan `helpText`, `defaultValue`, `isPrivate` e
|
|
142
|
+
`isVisible`. Usa además los ajustes correspondientes al tipo:
|
|
143
|
+
|
|
144
|
+
| Tipo | Ajustes principales |
|
|
145
|
+
| --- | --- |
|
|
146
|
+
| `text`, `textarea`, `slug` | `minLength`, `maxLength`, `pattern` |
|
|
147
|
+
| `rich_text`, `rich_text_full` | `richTextMode` |
|
|
148
|
+
| `media` | `mediaMultiple`, `acceptedMediaKinds` |
|
|
149
|
+
| `date` | `dateMode`: `date` o `datetime` |
|
|
150
|
+
| `number` | `min`, `max`, `step`, `precision` |
|
|
151
|
+
| `measure` | `measureDimensionCode`, `measureAllowedUnitCodes`, `measurePublicUnitCode` |
|
|
152
|
+
| `select` | `selectSource`, `selectOptions`, `selectSourceKey`, `selectMultiple` |
|
|
153
|
+
| `reference` | `referenceType`, `referenceContentTypeKey`, `referenceMultiple` |
|
|
154
|
+
| `repeater` | `minRows`, `maxRows` y `children` |
|
|
155
|
+
| `component` | `componentKey` |
|
|
156
|
+
|
|
157
|
+
Para una lista administrable usa `selectSource: "option_list"` y una key
|
|
158
|
+
devuelta por `getReferences().optionLists`. Para medidas usa exclusivamente los
|
|
159
|
+
códigos devueltos por `getReferences().measureDimensions`; no inventes unidades.
|
|
160
|
+
|
|
161
|
+
Una ficha técnica usa `usages: ["product_specification"]` y al menos un alcance
|
|
162
|
+
en `specificationScopes`. Sus campos técnicos pueden declarar
|
|
163
|
+
`isFilterable: true` únicamente cuando las reglas de Bacano lo permitan. Los
|
|
164
|
+
otros usos editoriales son `website_content` y `product_additional`.
|
|
165
|
+
|
|
166
|
+
## Ciclo de vida
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const preview = await content.inspectLifecycle("home_hero", "delete");
|
|
170
|
+
|
|
171
|
+
if (preview.allowed && userConfirmed) {
|
|
172
|
+
await content.applyLifecycle("home_hero", "delete");
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Nunca ignores `dependencies`, `allowed` o `errors`. Archivar es preferible a
|
|
177
|
+
eliminar cuando existe contenido histórico. El backend aplica las mismas reglas
|
|
178
|
+
de dependencias, keys, componentes, secciones y cambios destructivos que el
|
|
179
|
+
backoffice; el SDK no crea un camino alterno.
|
|
180
|
+
|
|
181
|
+
## Límites de esta versión
|
|
182
|
+
|
|
183
|
+
- No crea ni modifica entradas de Content.
|
|
184
|
+
- No carga archivos ni media.
|
|
185
|
+
- No crea listas de opciones.
|
|
186
|
+
- No funciona desde el cliente público del storefront.
|
|
187
|
+
- No acepta GraphQL directo ni credenciales administrativas permanentes.
|
|
188
|
+
|
|
189
|
+
El resultado esperado del flujo Figma es: el agente propone el blueprint, el
|
|
190
|
+
usuario habilita el acceso temporal, el agente crea o ajusta los tipos y
|
|
191
|
+
componentes aprobados, y el equipo editorial completa las entradas desde
|
|
192
|
+
Bacano.
|
package/README.md
CHANGED
|
@@ -21,6 +21,21 @@ React hooks are available from `@bacano/sdk/react` and require `react >= 18`
|
|
|
21
21
|
as a peer dependency. The core client has no runtime dependencies and works
|
|
22
22
|
without React.
|
|
23
23
|
|
|
24
|
+
### Gestión de tipos de Content para agentes
|
|
25
|
+
|
|
26
|
+
Un agente autorizado puede consultar, crear y mantener tipos y componentes con
|
|
27
|
+
el entrypoint administrativo `@bacano/sdk/content-management`. El acceso se
|
|
28
|
+
genera temporalmente desde Bacano, está limitado a una empresa y nunca debe
|
|
29
|
+
incluirse en el storefront:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createContentManagementClient } from "@bacano/sdk/content-management";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Lee [`CONTENT_TYPE_MANAGEMENT.md`](./CONTENT_TYPE_MANAGEMENT.md) antes de usarlo.
|
|
36
|
+
La guía contiene el flujo de autorización, contratos de campos, referencias,
|
|
37
|
+
cambios destructivos y límites de seguridad.
|
|
38
|
+
|
|
24
39
|
### Auditoría del storefront
|
|
25
40
|
|
|
26
41
|
El SDK incluye la skill oficial `bacano-web`. Instalarla al iniciar un
|
|
@@ -67,8 +82,10 @@ la presencia del token REST. El OAuth del MCP remoto debe confirmarse dentro del
|
|
|
67
82
|
agente con las herramientas de Figma. La extracción se conserva como snapshot
|
|
68
83
|
normalizado. Cuando el equipo de Content y desarrollo trabajan en tiempos
|
|
69
84
|
distintos, la skill genera un blueprint de entrega y permite fixtures locales
|
|
70
|
-
con el mismo contrato de los componentes.
|
|
71
|
-
desde Bacano
|
|
85
|
+
con el mismo contrato de los componentes. Las entradas reales continúan
|
|
86
|
+
creándose desde Bacano. Con autorización temporal, la skill puede crear o
|
|
87
|
+
ajustar tipos y componentes mediante el entrypoint administrativo oficial; un
|
|
88
|
+
fixture activo bloquea la aprobación de producción.
|
|
72
89
|
|
|
73
90
|
El snapshot puede validarse con:
|
|
74
91
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// libs/sdk/src/content-management.ts
|
|
21
|
+
var content_management_exports = {};
|
|
22
|
+
__export(content_management_exports, {
|
|
23
|
+
ContentManagementError: () => ContentManagementError,
|
|
24
|
+
createContentManagementClient: () => createContentManagementClient
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(content_management_exports);
|
|
27
|
+
var ContentManagementError = class extends Error {
|
|
28
|
+
code;
|
|
29
|
+
status;
|
|
30
|
+
details;
|
|
31
|
+
constructor(input) {
|
|
32
|
+
super(input.message);
|
|
33
|
+
this.name = "ContentManagementError";
|
|
34
|
+
this.code = input.code ?? "CONTENT_MANAGEMENT_ERROR";
|
|
35
|
+
this.status = input.status;
|
|
36
|
+
this.details = input.details;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function normalizeApiUrl(apiUrl) {
|
|
40
|
+
const normalized = apiUrl.trim().replace(/\/+$/, "");
|
|
41
|
+
if (!normalized) {
|
|
42
|
+
throw new ContentManagementError({
|
|
43
|
+
message: "apiUrl is required.",
|
|
44
|
+
code: "CONTENT_MANAGEMENT_CONFIG_INVALID",
|
|
45
|
+
status: 0
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
function encodeIdentifier(identifier) {
|
|
51
|
+
const normalized = identifier.trim();
|
|
52
|
+
if (!normalized) {
|
|
53
|
+
throw new ContentManagementError({
|
|
54
|
+
message: "A content type id or key is required.",
|
|
55
|
+
code: "CONTENT_TYPE_IDENTIFIER_INVALID",
|
|
56
|
+
status: 0
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return encodeURIComponent(normalized);
|
|
60
|
+
}
|
|
61
|
+
function createContentManagementClient(config) {
|
|
62
|
+
const apiUrl = normalizeApiUrl(config.apiUrl);
|
|
63
|
+
const fetchImplementation = config.fetch ?? globalThis.fetch;
|
|
64
|
+
if (!fetchImplementation) {
|
|
65
|
+
throw new ContentManagementError({
|
|
66
|
+
message: "A fetch implementation is required.",
|
|
67
|
+
code: "CONTENT_MANAGEMENT_CONFIG_INVALID",
|
|
68
|
+
status: 0
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const getAccessToken = async () => {
|
|
72
|
+
const token = typeof config.accessToken === "function" ? await config.accessToken() : config.accessToken;
|
|
73
|
+
const normalized = token.trim();
|
|
74
|
+
if (!normalized) {
|
|
75
|
+
throw new ContentManagementError({
|
|
76
|
+
message: "A temporary content management access token is required.",
|
|
77
|
+
code: "CONTENT_MANAGEMENT_ACCESS_REQUIRED",
|
|
78
|
+
status: 0
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return normalized;
|
|
82
|
+
};
|
|
83
|
+
const request = async (path, init) => {
|
|
84
|
+
const response = await fetchImplementation(
|
|
85
|
+
`${apiUrl}/api/v1/content-management${path}`,
|
|
86
|
+
{
|
|
87
|
+
...init,
|
|
88
|
+
headers: {
|
|
89
|
+
Accept: "application/json",
|
|
90
|
+
Authorization: `Bearer ${await getAccessToken()}`,
|
|
91
|
+
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
92
|
+
...init?.headers
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
let payload = null;
|
|
97
|
+
try {
|
|
98
|
+
payload = await response.json();
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok || !payload || payload.success !== true) {
|
|
102
|
+
const error = payload && "error" in payload ? payload.error : null;
|
|
103
|
+
throw new ContentManagementError({
|
|
104
|
+
message: error?.message ?? `Content management request failed with HTTP ${response.status}.`,
|
|
105
|
+
code: error?.code,
|
|
106
|
+
status: response.status,
|
|
107
|
+
details: error?.details
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return payload.data;
|
|
111
|
+
};
|
|
112
|
+
const lifecycle = (identifier, operation, dryRun) => request(
|
|
113
|
+
`/types/${encodeIdentifier(identifier)}/lifecycle`,
|
|
114
|
+
{
|
|
115
|
+
method: "POST",
|
|
116
|
+
body: JSON.stringify({ operation, dryRun })
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
return {
|
|
120
|
+
getContext: () => request("/context"),
|
|
121
|
+
getReferences: () => request("/references"),
|
|
122
|
+
listTypes: () => request("/types"),
|
|
123
|
+
getType: (identifier) => request(`/types/${encodeIdentifier(identifier)}`),
|
|
124
|
+
createType: (input) => request("/types", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
body: JSON.stringify(input)
|
|
127
|
+
}),
|
|
128
|
+
updateType: (identifier, input) => request(`/types/${encodeIdentifier(identifier)}`, {
|
|
129
|
+
method: "PUT",
|
|
130
|
+
body: JSON.stringify(input)
|
|
131
|
+
}),
|
|
132
|
+
inspectLifecycle: (identifier, operation) => lifecycle(identifier, operation, true),
|
|
133
|
+
applyLifecycle: (identifier, operation) => lifecycle(identifier, operation, false)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export type ContentManagementTypeKind = "entry" | "component";
|
|
2
|
+
export type ContentManagementTypeStatus = "draft" | "active" | "archived";
|
|
3
|
+
export type ContentManagementTypeUsage = "website_content" | "product_additional" | "product_specification";
|
|
4
|
+
export type ContentManagementFieldType = "boolean" | "component" | "date" | "link" | "measure" | "media" | "number" | "reference" | "repeater" | "rich_text" | "rich_text_full" | "select" | "slug" | "text" | "textarea";
|
|
5
|
+
export interface ContentManagementChildFieldInput {
|
|
6
|
+
id?: string | null;
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
type: ContentManagementFieldType;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
sortOrder: number;
|
|
12
|
+
settings?: Record<string, unknown>;
|
|
13
|
+
componentKey?: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface ContentManagementFieldInput extends ContentManagementChildFieldInput {
|
|
16
|
+
sectionKey?: string | null;
|
|
17
|
+
children?: ContentManagementChildFieldInput[];
|
|
18
|
+
}
|
|
19
|
+
export interface ContentManagementTypeInput {
|
|
20
|
+
key: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
kind: ContentManagementTypeKind;
|
|
24
|
+
status?: "draft" | "active";
|
|
25
|
+
settings?: Record<string, unknown>;
|
|
26
|
+
usages?: ContentManagementTypeUsage[];
|
|
27
|
+
sections?: Array<{
|
|
28
|
+
key: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
sortOrder: number;
|
|
32
|
+
}>;
|
|
33
|
+
specificationScopes?: Array<{
|
|
34
|
+
category?: string | null;
|
|
35
|
+
subcategoryId?: string | null;
|
|
36
|
+
}>;
|
|
37
|
+
deletedPaths?: string[];
|
|
38
|
+
confirmDestructiveChanges?: boolean;
|
|
39
|
+
fields?: ContentManagementFieldInput[];
|
|
40
|
+
}
|
|
41
|
+
export interface ContentManagementType extends Omit<ContentManagementTypeInput, "status"> {
|
|
42
|
+
id: string;
|
|
43
|
+
companyId: string;
|
|
44
|
+
description: string;
|
|
45
|
+
status: ContentManagementTypeStatus;
|
|
46
|
+
settings: Record<string, unknown>;
|
|
47
|
+
usages: ContentManagementTypeUsage[];
|
|
48
|
+
sections: Array<{
|
|
49
|
+
id: string;
|
|
50
|
+
key: string;
|
|
51
|
+
title: string;
|
|
52
|
+
description: string;
|
|
53
|
+
sortOrder: number;
|
|
54
|
+
}>;
|
|
55
|
+
specificationScopes: Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
category: string | null;
|
|
58
|
+
subcategoryId: string | null;
|
|
59
|
+
}>;
|
|
60
|
+
fields: ContentManagementFieldInput[];
|
|
61
|
+
createdAt: string;
|
|
62
|
+
updatedAt: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ContentManagementContext {
|
|
65
|
+
companyId: string;
|
|
66
|
+
scope: "content_types:manage";
|
|
67
|
+
expiresAt: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ContentManagementReferences {
|
|
70
|
+
categories: Array<{
|
|
71
|
+
code: string;
|
|
72
|
+
name: string;
|
|
73
|
+
subcategories: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
}>;
|
|
77
|
+
}>;
|
|
78
|
+
optionLists: Array<{
|
|
79
|
+
key: string;
|
|
80
|
+
name: string;
|
|
81
|
+
isExtensible: boolean;
|
|
82
|
+
options: Array<{
|
|
83
|
+
value: string;
|
|
84
|
+
label: string;
|
|
85
|
+
}>;
|
|
86
|
+
}>;
|
|
87
|
+
measureDimensions: Array<{
|
|
88
|
+
code: string;
|
|
89
|
+
name: string;
|
|
90
|
+
canonicalUnitCode: string;
|
|
91
|
+
units: Array<{
|
|
92
|
+
code: string;
|
|
93
|
+
name: string;
|
|
94
|
+
symbol: string;
|
|
95
|
+
}>;
|
|
96
|
+
}>;
|
|
97
|
+
}
|
|
98
|
+
export type ContentManagementLifecycleOperation = "archive" | "restore" | "delete";
|
|
99
|
+
export interface ContentManagementLifecycleResult {
|
|
100
|
+
success: boolean;
|
|
101
|
+
allowed: boolean;
|
|
102
|
+
applied: boolean;
|
|
103
|
+
entity_id: string | null;
|
|
104
|
+
entity_type: "type" | "component" | null;
|
|
105
|
+
operation: ContentManagementLifecycleOperation | null;
|
|
106
|
+
name: string | null;
|
|
107
|
+
status: string | null;
|
|
108
|
+
dependencies: Array<{
|
|
109
|
+
code: string;
|
|
110
|
+
label: string;
|
|
111
|
+
count: number;
|
|
112
|
+
blocking: boolean;
|
|
113
|
+
}>;
|
|
114
|
+
message: string | null;
|
|
115
|
+
errors: string[];
|
|
116
|
+
}
|
|
117
|
+
export type ContentManagementAccessTokenProvider = () => string | Promise<string>;
|
|
118
|
+
export interface ContentManagementClientConfig {
|
|
119
|
+
apiUrl: string;
|
|
120
|
+
accessToken: string | ContentManagementAccessTokenProvider;
|
|
121
|
+
fetch?: typeof fetch;
|
|
122
|
+
}
|
|
123
|
+
export declare class ContentManagementError extends Error {
|
|
124
|
+
readonly code: string;
|
|
125
|
+
readonly status: number;
|
|
126
|
+
readonly details: unknown;
|
|
127
|
+
constructor(input: {
|
|
128
|
+
message: string;
|
|
129
|
+
code?: string;
|
|
130
|
+
status: number;
|
|
131
|
+
details?: unknown;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
export interface ContentManagementClient {
|
|
135
|
+
getContext(): Promise<ContentManagementContext>;
|
|
136
|
+
getReferences(): Promise<ContentManagementReferences>;
|
|
137
|
+
listTypes(): Promise<ContentManagementType[]>;
|
|
138
|
+
getType(identifier: string): Promise<ContentManagementType>;
|
|
139
|
+
createType(input: ContentManagementTypeInput): Promise<ContentManagementType>;
|
|
140
|
+
updateType(identifier: string, input: ContentManagementTypeInput): Promise<ContentManagementType>;
|
|
141
|
+
inspectLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
142
|
+
applyLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
143
|
+
}
|
|
144
|
+
export declare function createContentManagementClient(config: ContentManagementClientConfig): ContentManagementClient;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// libs/sdk/src/content-management.ts
|
|
2
|
+
var ContentManagementError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(input) {
|
|
7
|
+
super(input.message);
|
|
8
|
+
this.name = "ContentManagementError";
|
|
9
|
+
this.code = input.code ?? "CONTENT_MANAGEMENT_ERROR";
|
|
10
|
+
this.status = input.status;
|
|
11
|
+
this.details = input.details;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function normalizeApiUrl(apiUrl) {
|
|
15
|
+
const normalized = apiUrl.trim().replace(/\/+$/, "");
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
throw new ContentManagementError({
|
|
18
|
+
message: "apiUrl is required.",
|
|
19
|
+
code: "CONTENT_MANAGEMENT_CONFIG_INVALID",
|
|
20
|
+
status: 0
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
function encodeIdentifier(identifier) {
|
|
26
|
+
const normalized = identifier.trim();
|
|
27
|
+
if (!normalized) {
|
|
28
|
+
throw new ContentManagementError({
|
|
29
|
+
message: "A content type id or key is required.",
|
|
30
|
+
code: "CONTENT_TYPE_IDENTIFIER_INVALID",
|
|
31
|
+
status: 0
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return encodeURIComponent(normalized);
|
|
35
|
+
}
|
|
36
|
+
function createContentManagementClient(config) {
|
|
37
|
+
const apiUrl = normalizeApiUrl(config.apiUrl);
|
|
38
|
+
const fetchImplementation = config.fetch ?? globalThis.fetch;
|
|
39
|
+
if (!fetchImplementation) {
|
|
40
|
+
throw new ContentManagementError({
|
|
41
|
+
message: "A fetch implementation is required.",
|
|
42
|
+
code: "CONTENT_MANAGEMENT_CONFIG_INVALID",
|
|
43
|
+
status: 0
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const getAccessToken = async () => {
|
|
47
|
+
const token = typeof config.accessToken === "function" ? await config.accessToken() : config.accessToken;
|
|
48
|
+
const normalized = token.trim();
|
|
49
|
+
if (!normalized) {
|
|
50
|
+
throw new ContentManagementError({
|
|
51
|
+
message: "A temporary content management access token is required.",
|
|
52
|
+
code: "CONTENT_MANAGEMENT_ACCESS_REQUIRED",
|
|
53
|
+
status: 0
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
57
|
+
};
|
|
58
|
+
const request = async (path, init) => {
|
|
59
|
+
const response = await fetchImplementation(
|
|
60
|
+
`${apiUrl}/api/v1/content-management${path}`,
|
|
61
|
+
{
|
|
62
|
+
...init,
|
|
63
|
+
headers: {
|
|
64
|
+
Accept: "application/json",
|
|
65
|
+
Authorization: `Bearer ${await getAccessToken()}`,
|
|
66
|
+
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
67
|
+
...init?.headers
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
let payload = null;
|
|
72
|
+
try {
|
|
73
|
+
payload = await response.json();
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
if (!response.ok || !payload || payload.success !== true) {
|
|
77
|
+
const error = payload && "error" in payload ? payload.error : null;
|
|
78
|
+
throw new ContentManagementError({
|
|
79
|
+
message: error?.message ?? `Content management request failed with HTTP ${response.status}.`,
|
|
80
|
+
code: error?.code,
|
|
81
|
+
status: response.status,
|
|
82
|
+
details: error?.details
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return payload.data;
|
|
86
|
+
};
|
|
87
|
+
const lifecycle = (identifier, operation, dryRun) => request(
|
|
88
|
+
`/types/${encodeIdentifier(identifier)}/lifecycle`,
|
|
89
|
+
{
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: JSON.stringify({ operation, dryRun })
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
return {
|
|
95
|
+
getContext: () => request("/context"),
|
|
96
|
+
getReferences: () => request("/references"),
|
|
97
|
+
listTypes: () => request("/types"),
|
|
98
|
+
getType: (identifier) => request(`/types/${encodeIdentifier(identifier)}`),
|
|
99
|
+
createType: (input) => request("/types", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: JSON.stringify(input)
|
|
102
|
+
}),
|
|
103
|
+
updateType: (identifier, input) => request(`/types/${encodeIdentifier(identifier)}`, {
|
|
104
|
+
method: "PUT",
|
|
105
|
+
body: JSON.stringify(input)
|
|
106
|
+
}),
|
|
107
|
+
inspectLifecycle: (identifier, operation) => lifecycle(identifier, operation, true),
|
|
108
|
+
applyLifecycle: (identifier, operation) => lifecycle(identifier, operation, false)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
ContentManagementError,
|
|
113
|
+
createContentManagementClient
|
|
114
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacano/sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"description": "TypeScript SDK for building websites on the Bacano ERP",
|
|
5
5
|
"main": "./index.cjs",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"import": "./graphql.js",
|
|
24
24
|
"require": "./graphql.cjs"
|
|
25
25
|
},
|
|
26
|
+
"./content-management": {
|
|
27
|
+
"types": "./content-management.d.ts",
|
|
28
|
+
"import": "./content-management.js",
|
|
29
|
+
"require": "./content-management.cjs"
|
|
30
|
+
},
|
|
26
31
|
"./react": {
|
|
27
32
|
"types": "./react.d.ts",
|
|
28
33
|
"import": "./react.js",
|
|
@@ -93,9 +93,12 @@ Si Content todavía no existe, generar un blueprint de entrega y fixtures
|
|
|
93
93
|
temporales fieles al contenido visible de Figma. Los fixtures deben usar el
|
|
94
94
|
mismo view model que consumen los componentes y entrar por un único adaptador;
|
|
95
95
|
nunca repartir datos de prueba dentro de la UI. Si Content ya existe, mapearlo
|
|
96
|
-
sin sobrescribir estructuras o entradas.
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
sin sobrescribir estructuras o entradas. Si el usuario pide materializar el
|
|
97
|
+
blueprint, leer la sección de automatización de
|
|
98
|
+
[references/content-workflow.md](references/content-workflow.md) y el contrato
|
|
99
|
+
`CONTENT_TYPE_MANAGEMENT.md` incluido en `@bacano/sdk`. Solo crear o modificar
|
|
100
|
+
tipos y componentes mediante el acceso temporal oficial; las entradas y media
|
|
101
|
+
permanecen en el backoffice.
|
|
99
102
|
|
|
100
103
|
Antes de modificar código, indicar brevemente qué rutas y componentes se
|
|
101
104
|
implementarán y qué decisiones de diseño son inferidas.
|
|
@@ -129,10 +129,12 @@ Entregar una tabla legible con:
|
|
|
129
129
|
- Estado y responsable.
|
|
130
130
|
- Dependencias entre componentes, listas y entradas.
|
|
131
131
|
|
|
132
|
-
El equipo de Content
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
El equipo de Content puede crear o ajustar la estructura desde Bacano. Si el
|
|
133
|
+
usuario autoriza que el agente materialice el blueprint, usar exclusivamente
|
|
134
|
+
`@bacano/sdk/content-management` y el token temporal generado desde **Conectar
|
|
135
|
+
agente**. Confirmar la empresa, inventariar primero, crear componentes antes de
|
|
136
|
+
sus consumidores y trabajar inicialmente en borrador. La skill no crea entradas,
|
|
137
|
+
importaciones ni cargas de media.
|
|
136
138
|
|
|
137
139
|
## 6. Cambio a Bacano real
|
|
138
140
|
|
|
@@ -149,10 +151,19 @@ depende de contenido temporal, reportar `❌ NO LISTO` con las rutas y secciones
|
|
|
149
151
|
afectadas. Los fixtures pueden conservarse como datos de pruebas unitarias o
|
|
150
152
|
visuales siempre que no entren en el bundle o loader de producción.
|
|
151
153
|
|
|
152
|
-
## 7. Automatización
|
|
154
|
+
## 7. Automatización administrativa
|
|
153
155
|
|
|
154
|
-
La
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
La gestión de tipos pertenece al entrypoint separado
|
|
157
|
+
`@bacano/sdk/content-management`, no al cliente público del storefront. Antes de
|
|
158
|
+
escribir:
|
|
159
|
+
|
|
160
|
+
1. Pedir al usuario que genere un acceso temporal de 15, 30 o 60 minutos.
|
|
161
|
+
2. Guardarlo solo en el entorno local del proceso del agente.
|
|
162
|
+
3. Ejecutar `getContext()`, `listTypes()` y `getReferences()`.
|
|
163
|
+
4. Mostrar el plan y esperar aprobación cuando altere estructuras existentes.
|
|
164
|
+
5. Crear primero componentes y luego tipos consumidores, siempre en borrador.
|
|
165
|
+
6. Para archivar o eliminar, inspeccionar dependencias y confirmar nuevamente.
|
|
166
|
+
|
|
167
|
+
Nunca exponer el token en el navegador, commit, blueprint, salida de consola o
|
|
168
|
+
chat. Nunca convertir el blueprint en una importación implícita ni usar GraphQL
|
|
169
|
+
directo. Las entradas y media permanecen bajo gestión editorial en Bacano.
|