@deveye/types 0.15.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/LICENSE +21 -0
- package/README.md +23 -0
- package/package.json +68 -0
- package/src/domain/audience.ts +549 -0
- package/src/domain/backup.ts +355 -0
- package/src/domain/credential.ts +55 -0
- package/src/domain/database.ts +467 -0
- package/src/domain/deploy.ts +231 -0
- package/src/domain/device.ts +172 -0
- package/src/domain/deviceFiles.ts +84 -0
- package/src/domain/deviceLogs.ts +82 -0
- package/src/domain/featureRegistry.ts +392 -0
- package/src/domain/finance.ts +477 -0
- package/src/domain/git.ts +419 -0
- package/src/domain/home.ts +314 -0
- package/src/domain/live.ts +272 -0
- package/src/domain/logs.ts +117 -0
- package/src/domain/mail.ts +394 -0
- package/src/domain/metrics.ts +127 -0
- package/src/domain/note.ts +202 -0
- package/src/domain/notifications.ts +268 -0
- package/src/domain/packages.ts +35 -0
- package/src/domain/password.ts +36 -0
- package/src/domain/presence.ts +21 -0
- package/src/domain/project.ts +168 -0
- package/src/domain/projectBoard.ts +130 -0
- package/src/domain/projectChat.ts +46 -0
- package/src/domain/projectHistory.ts +82 -0
- package/src/domain/projectLink.ts +87 -0
- package/src/domain/projectPlan.ts +68 -0
- package/src/domain/report.ts +492 -0
- package/src/domain/role.ts +8 -0
- package/src/domain/secrecy.ts +66 -0
- package/src/domain/sentinel.ts +623 -0
- package/src/domain/sharing.ts +186 -0
- package/src/domain/syncProtocol.ts +116 -0
- package/src/domain/twoFactor.ts +40 -0
- package/src/domain/uptime.ts +216 -0
- package/src/domain/user.ts +141 -0
- package/src/domain/workspace.ts +56 -0
- package/src/domain/workspaceRole.ts +251 -0
- package/src/features/admin.ts +112 -0
- package/src/features/audience.ts +275 -0
- package/src/features/backup.ts +230 -0
- package/src/features/database.ts +461 -0
- package/src/features/deploy.ts +245 -0
- package/src/features/device.ts +292 -0
- package/src/features/deviceFiles.ts +83 -0
- package/src/features/deviceLogs.ts +36 -0
- package/src/features/deviceTerminal.ts +57 -0
- package/src/features/finance.ts +360 -0
- package/src/features/git.ts +368 -0
- package/src/features/home.ts +32 -0
- package/src/features/live.ts +113 -0
- package/src/features/logs.ts +86 -0
- package/src/features/mail.ts +374 -0
- package/src/features/metrics.ts +185 -0
- package/src/features/note.ts +189 -0
- package/src/features/notify.ts +164 -0
- package/src/features/password.ts +67 -0
- package/src/features/project.ts +709 -0
- package/src/features/registry.ts +103 -0
- package/src/features/secrecy.ts +120 -0
- package/src/features/sentinel.ts +233 -0
- package/src/features/sharing.ts +79 -0
- package/src/features/twoFactor.ts +47 -0
- package/src/features/uptime.ts +186 -0
- package/src/features/user.ts +91 -0
- package/src/features/workspace.ts +200 -0
- package/src/http/auth.ts +94 -0
- package/src/http/device.ts +222 -0
- package/src/http/status.ts +45 -0
- package/src/index.ts +1700 -0
- package/src/protocol/agent.ts +1171 -0
- package/src/protocol/envelope.ts +46 -0
- package/src/protocol/error.ts +27 -0
- package/src/protocol/result.ts +17 -0
- package/src/protocol/version.ts +6 -0
- package/src/sdk/client-ambient.d.ts +238 -0
- package/src/sdk/client.ts +66 -0
- package/src/sdk/ids.ts +25 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/manifest.ts +326 -0
- package/src/sdk/providers.ts +48 -0
- package/src/sdk/server.ts +378 -0
- package/src/sdk/testing.ts +179 -0
- package/src/utils/version.ts +28 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { projectStatusSchema } from './project';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Les bases de données d'un espace.
|
|
6
|
+
*
|
|
7
|
+
* Même renversement que pour les dépôts git : une base appartient à l'espace,
|
|
8
|
+
* pas à un projet, et plusieurs projets peuvent pointer la même. Elle vit donc à
|
|
9
|
+
* l'étage **ouvert** du chiffrement — un projet confidentiel ne peut pas en
|
|
10
|
+
* lier, exactement comme pour un dépôt.
|
|
11
|
+
*
|
|
12
|
+
* ## Ce qui est chiffré, et ce qui ne sort jamais
|
|
13
|
+
*
|
|
14
|
+
* `content` porte l'adresse, le port, le nom de la base et l'identifiant, tous
|
|
15
|
+
* chiffrés. Le **mot de passe** vit à part, dans sa propre colonne, et ne quitte
|
|
16
|
+
* jamais le serveur : les DTO ci-dessous n'en portent qu'un `hasPassword`. Même
|
|
17
|
+
* règle pour le secret du tunnel. C'est la même discipline que les jetons
|
|
18
|
+
* d'accès git, et pour la même raison — un secret rendu au client est un secret
|
|
19
|
+
* qu'on ne peut plus reprendre.
|
|
20
|
+
*
|
|
21
|
+
* ## À la demande par défaut
|
|
22
|
+
*
|
|
23
|
+
* Rien ne se connecte tout seul : ouvrir la feature ne joint aucune base. Le
|
|
24
|
+
* relevé périodique (`monitorEnabled`) est une option, activée base par base, et
|
|
25
|
+
* c'est **seulement** quand elle est active que les alertes ont un sens — il
|
|
26
|
+
* faut bien que quelque chose les évalue.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const DATABASE_NAME_MAX_LENGTH = 96;
|
|
30
|
+
export const DATABASE_HOST_MAX_LENGTH = 255;
|
|
31
|
+
export const DATABASE_USER_MAX_LENGTH = 128;
|
|
32
|
+
export const DATABASE_SECRET_MAX_LENGTH = 8192;
|
|
33
|
+
export const DATABASE_ALERT_NAME_MAX_LENGTH = 96;
|
|
34
|
+
export const DATABASE_ALERT_MESSAGE_MAX_LENGTH = 1000;
|
|
35
|
+
export const DATABASE_SQL_MAX_LENGTH = 4000;
|
|
36
|
+
|
|
37
|
+
/** Les moteurs joignables. Deux dialectes, deux adaptateurs, rien d'autre. */
|
|
38
|
+
export const databaseEngineSchema = z.enum(['mysql', 'postgres']);
|
|
39
|
+
export type DatabaseEngine = z.infer<typeof databaseEngineSchema>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Par où passe la connexion.
|
|
43
|
+
*
|
|
44
|
+
* `direct` — le serveur joint l'hôte lui-même.
|
|
45
|
+
* `ssh` — un tunnel TCP est ouvert dans le processus, sans binaire externe ni
|
|
46
|
+
* fichier de clé sur disque.
|
|
47
|
+
* `socks` — la connexion transite par un proxy SOCKS5 déjà en place.
|
|
48
|
+
*/
|
|
49
|
+
export const databaseAccessKindSchema = z.enum(['direct', 'ssh', 'socks']);
|
|
50
|
+
export type DatabaseAccessKind = z.infer<typeof databaseAccessKindSchema>;
|
|
51
|
+
|
|
52
|
+
/** Comment le tunnel s'authentifie, quand il y en a un. */
|
|
53
|
+
export const databaseSshAuthSchema = z.enum(['password', 'key']);
|
|
54
|
+
export type DatabaseSshAuth = z.infer<typeof databaseSshAuthSchema>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* L'état de la dernière connexion connue.
|
|
58
|
+
*
|
|
59
|
+
* `unknown` n'est pas une panne : c'est l'état normal d'une base qu'on n'a
|
|
60
|
+
* jamais jointe, ce qui est le cas par défaut de toutes. Le confondre avec
|
|
61
|
+
* `down` ferait passer une feature au repos pour une feature en alerte.
|
|
62
|
+
*/
|
|
63
|
+
export const databaseStatusSchema = z.enum(['unknown', 'up', 'down']);
|
|
64
|
+
export type DatabaseStatus = z.infer<typeof databaseStatusSchema>;
|
|
65
|
+
|
|
66
|
+
/** Les réglages du tunnel, sans aucun secret. */
|
|
67
|
+
export const databaseAccessSchema = z.object({
|
|
68
|
+
kind: databaseAccessKindSchema,
|
|
69
|
+
/** Hôte du rebond SSH ou du proxy SOCKS ; vide en accès direct. */
|
|
70
|
+
host: z.string().max(DATABASE_HOST_MAX_LENGTH),
|
|
71
|
+
port: z.number().int().min(1).max(65535).nullable(),
|
|
72
|
+
/** Utilisateur SSH ; vide pour un proxy SOCKS anonyme. */
|
|
73
|
+
username: z.string().max(DATABASE_USER_MAX_LENGTH),
|
|
74
|
+
auth: databaseSshAuthSchema,
|
|
75
|
+
/** Un secret est enregistré (mot de passe ou clé privée) — jamais lequel. */
|
|
76
|
+
hasSecret: z.boolean()
|
|
77
|
+
});
|
|
78
|
+
export type DatabaseAccess = z.infer<typeof databaseAccessSchema>;
|
|
79
|
+
|
|
80
|
+
export const databaseSchema = z.object({
|
|
81
|
+
id: z.number().int().positive(),
|
|
82
|
+
engine: databaseEngineSchema,
|
|
83
|
+
/** Le nom que lui donne l'utilisateur ; porte l'unicité dans l'espace. */
|
|
84
|
+
name: z.string().max(DATABASE_NAME_MAX_LENGTH),
|
|
85
|
+
host: z.string().max(DATABASE_HOST_MAX_LENGTH),
|
|
86
|
+
port: z.number().int().min(1).max(65535),
|
|
87
|
+
/** Le nom de la base sur le serveur (`schema` chez PostgreSQL). */
|
|
88
|
+
database: z.string().max(DATABASE_NAME_MAX_LENGTH),
|
|
89
|
+
username: z.string().max(DATABASE_USER_MAX_LENGTH),
|
|
90
|
+
/** Un mot de passe est enregistré — jamais lequel. */
|
|
91
|
+
/**
|
|
92
|
+
* Cet élément vient d'un **autre espace**, qui le projette ici.
|
|
93
|
+
*
|
|
94
|
+
* L'écran le signale d'une pastille : sans elle, rien ne distingue une
|
|
95
|
+
* ligne locale d'une fenêtre sur l'espace voisin — et les gestes réservés
|
|
96
|
+
* au domicile (supprimer, re-partager) sembleraient cassés au lieu de
|
|
97
|
+
* s'expliquer.
|
|
98
|
+
*/
|
|
99
|
+
foreign: z.boolean(),
|
|
100
|
+
hasPassword: z.boolean(),
|
|
101
|
+
access: databaseAccessSchema,
|
|
102
|
+
/** Le relevé périodique tourne-t-il ? Désactivé par défaut. */
|
|
103
|
+
monitorEnabled: z.boolean(),
|
|
104
|
+
/** Cadence du relevé, en secondes. Sans effet si le relevé est éteint. */
|
|
105
|
+
intervalSeconds: z.number().int().positive(),
|
|
106
|
+
/**
|
|
107
|
+
* Charger l'inventaire des tables dès l'ouverture de la fiche.
|
|
108
|
+
*
|
|
109
|
+
* Éteint par défaut, comme tout ce qui joint un serveur dans cette feature.
|
|
110
|
+
* Allumé, c'est le **seul** endroit où une connexion part sans qu'on ait
|
|
111
|
+
* cliqué — d'où le réglage par base plutôt qu'un comportement global.
|
|
112
|
+
*/
|
|
113
|
+
autoLoadTables: z.boolean(),
|
|
114
|
+
lastCheckAt: z.number().int().nullable(),
|
|
115
|
+
/**
|
|
116
|
+
* Ce qu'a duré le dernier relevé, en millisecondes.
|
|
117
|
+
*
|
|
118
|
+
* Mesuré de l'ouverture de la connexion à la fin de l'inventaire : c'est le
|
|
119
|
+
* temps de réponse **du serveur tel qu'on l'atteint**, tunnel compris, et
|
|
120
|
+
* non celui d'une requête isolée. Renseigné même sur un échec — un relevé
|
|
121
|
+
* qui met douze secondes à échouer dit quelque chose qu'un simple
|
|
122
|
+
* « injoignable » ne dit pas.
|
|
123
|
+
*/
|
|
124
|
+
lastElapsedMs: z.number().int().nonnegative().nullable(),
|
|
125
|
+
status: databaseStatusSchema,
|
|
126
|
+
/** Message du dernier échec, ou `null` après un succès. */
|
|
127
|
+
lastError: z.string().nullable(),
|
|
128
|
+
serverVersion: z.string().nullable(),
|
|
129
|
+
sizeBytes: z.number().int().nonnegative().nullable(),
|
|
130
|
+
tableCount: z.number().int().nonnegative().nullable(),
|
|
131
|
+
/** Combien d'alertes sont définies, et combien sont actuellement franchies. */
|
|
132
|
+
alertCount: z.number().int().nonnegative(),
|
|
133
|
+
firingCount: z.number().int().nonnegative(),
|
|
134
|
+
/** Combien de projets s'en servent — l'interconnexion, comme pour un dépôt. */
|
|
135
|
+
projectCount: z.number().int().nonnegative(),
|
|
136
|
+
created: z.number().int()
|
|
137
|
+
});
|
|
138
|
+
export type Database = z.infer<typeof databaseSchema>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Un projet qui utilise cette base.
|
|
142
|
+
*
|
|
143
|
+
* Ne remonte que des projets à l'étage ouvert — un projet confidentiel ne peut
|
|
144
|
+
* pas être lié, donc le titre est toujours lisible sans session.
|
|
145
|
+
*/
|
|
146
|
+
export const databaseUsageSchema = z.object({
|
|
147
|
+
projectId: z.number().int().positive(),
|
|
148
|
+
title: z.string(),
|
|
149
|
+
status: projectStatusSchema
|
|
150
|
+
});
|
|
151
|
+
export type DatabaseUsage = z.infer<typeof databaseUsageSchema>;
|
|
152
|
+
|
|
153
|
+
/** Le résultat d'un essai de connexion, à la demande. */
|
|
154
|
+
export const databaseProbeSchema = z.object({
|
|
155
|
+
ok: z.boolean(),
|
|
156
|
+
serverVersion: z.string().nullable(),
|
|
157
|
+
elapsedMs: z.number().int().nonnegative(),
|
|
158
|
+
/** Message clair, déjà traduit ; `null` en cas de succès. */
|
|
159
|
+
error: z.string().nullable()
|
|
160
|
+
});
|
|
161
|
+
export type DatabaseProbe = z.infer<typeof databaseProbeSchema>;
|
|
162
|
+
|
|
163
|
+
/** Une table, telle que l'exploration manuelle la montre. */
|
|
164
|
+
export const databaseTableSchema = z.object({
|
|
165
|
+
schema: z.string(),
|
|
166
|
+
name: z.string(),
|
|
167
|
+
/**
|
|
168
|
+
* Nombre de lignes **estimé**, tel que le moteur le tient dans ses
|
|
169
|
+
* statistiques : un `COUNT(*)` exact sur chaque table d'un serveur de
|
|
170
|
+
* production coûterait bien plus que ce que cette colonne apporte. `null`
|
|
171
|
+
* quand le moteur n'a pas encore analysé la table.
|
|
172
|
+
*/
|
|
173
|
+
rowCount: z.number().int().nonnegative().nullable(),
|
|
174
|
+
sizeBytes: z.number().int().nonnegative().nullable()
|
|
175
|
+
});
|
|
176
|
+
export type DatabaseTable = z.infer<typeof databaseTableSchema>;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Le contenu d'une table, ou le résultat d'une requête.
|
|
180
|
+
*
|
|
181
|
+
* Les valeurs voyagent en **chaînes**, jamais dans leur type d'origine : un
|
|
182
|
+
* `BIGINT` dépasse le nombre sûr de JavaScript, une date n'a pas la même forme
|
|
183
|
+
* chez les deux moteurs, et un `BLOB` n'a aucune représentation JSON. Le
|
|
184
|
+
* formatage appartient à l'affichage ; le transport, lui, doit être fidèle.
|
|
185
|
+
*/
|
|
186
|
+
export const databaseRowsSchema = z.object({
|
|
187
|
+
columns: z.array(z.string()),
|
|
188
|
+
rows: z.array(z.array(z.string().nullable())),
|
|
189
|
+
/** Total de lignes de la table, si le moteur a pu le donner. */
|
|
190
|
+
total: z.number().int().nonnegative().nullable(),
|
|
191
|
+
elapsedMs: z.number().int().nonnegative()
|
|
192
|
+
});
|
|
193
|
+
export type DatabaseRows = z.infer<typeof databaseRowsSchema>;
|
|
194
|
+
|
|
195
|
+
// ------------------------------------------------------------- structure
|
|
196
|
+
|
|
197
|
+
/** Une colonne, telle que le catalogue du moteur la décrit. */
|
|
198
|
+
export const databaseColumnSchema = z.object({
|
|
199
|
+
name: z.string(),
|
|
200
|
+
/** Le type tel que le moteur le nomme : `varchar(255)`, `int unsigned`… */
|
|
201
|
+
type: z.string(),
|
|
202
|
+
nullable: z.boolean(),
|
|
203
|
+
/** L'expression par défaut, telle quelle ; `null` quand il n'y en a pas. */
|
|
204
|
+
default: z.string().nullable(),
|
|
205
|
+
primaryKey: z.boolean(),
|
|
206
|
+
/**
|
|
207
|
+
* Le moteur la remplit seul : auto-incrément, identité, colonne générée.
|
|
208
|
+
* Le formulaire d'ajout ne la propose donc pas — la renseigner à la main
|
|
209
|
+
* serait au mieux ignoré, au pire refusé.
|
|
210
|
+
*/
|
|
211
|
+
generated: z.boolean(),
|
|
212
|
+
comment: z.string()
|
|
213
|
+
});
|
|
214
|
+
export type DatabaseColumn = z.infer<typeof databaseColumnSchema>;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Une clé étrangère, et ce qu'elle vise.
|
|
218
|
+
*
|
|
219
|
+
* `columns` et `refColumns` sont **appariées par position** : la première de
|
|
220
|
+
* l'une pointe la première de l'autre. C'est ce qui permet de suivre une
|
|
221
|
+
* contrainte composite sans deviner.
|
|
222
|
+
*/
|
|
223
|
+
export const databaseForeignKeySchema = z.object({
|
|
224
|
+
name: z.string(),
|
|
225
|
+
columns: z.array(z.string()).min(1),
|
|
226
|
+
refSchema: z.string(),
|
|
227
|
+
refTable: z.string(),
|
|
228
|
+
refColumns: z.array(z.string()).min(1)
|
|
229
|
+
});
|
|
230
|
+
export type DatabaseForeignKey = z.infer<typeof databaseForeignKeySchema>;
|
|
231
|
+
|
|
232
|
+
/** Un index, clé primaire exclue — celle-ci est portée par les colonnes. */
|
|
233
|
+
export const databaseIndexSchema = z.object({
|
|
234
|
+
name: z.string(),
|
|
235
|
+
columns: z.array(z.string()),
|
|
236
|
+
unique: z.boolean()
|
|
237
|
+
});
|
|
238
|
+
export type DatabaseIndex = z.infer<typeof databaseIndexSchema>;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* La structure d'une table : ce qu'il faut pour la lire, l'écrire et la suivre.
|
|
242
|
+
*
|
|
243
|
+
* Un seul objet parce que ses trois usages sont indissociables : le panneau
|
|
244
|
+
* « Structure » l'affiche, le formulaire de ligne en tire ses champs, et la
|
|
245
|
+
* navigation par clé étrangère en tire ses liens. Les charger séparément
|
|
246
|
+
* multiplierait les allers-retours pour une même sélection de table.
|
|
247
|
+
*/
|
|
248
|
+
export const databaseStructureSchema = z.object({
|
|
249
|
+
schema: z.string(),
|
|
250
|
+
table: z.string(),
|
|
251
|
+
columns: z.array(databaseColumnSchema),
|
|
252
|
+
/**
|
|
253
|
+
* Les colonnes qui désignent une ligne, dans l'ordre de la clé.
|
|
254
|
+
*
|
|
255
|
+
* **Vide = pas de clé primaire**, et c'est décisif : sans elle, aucune
|
|
256
|
+
* modification ni suppression n'est proposée. Une table sans clé ne permet
|
|
257
|
+
* pas de nommer *une* ligne, et un `DELETE` qui en emporterait deux est un
|
|
258
|
+
* accident qu'on ne peut pas rattraper.
|
|
259
|
+
*/
|
|
260
|
+
primaryKey: z.array(z.string()),
|
|
261
|
+
foreignKeys: z.array(databaseForeignKeySchema),
|
|
262
|
+
indexes: z.array(databaseIndexSchema)
|
|
263
|
+
});
|
|
264
|
+
export type DatabaseStructure = z.infer<typeof databaseStructureSchema>;
|
|
265
|
+
|
|
266
|
+
// --------------------------------------------------------------- recherche
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Comment une colonne est confrontée à une valeur.
|
|
270
|
+
*
|
|
271
|
+
* Fermé, et c'est le point : la recherche ne transporte jamais de fragment de
|
|
272
|
+
* SQL. Le serveur choisit l'opérateur dans cette liste et lie la valeur en
|
|
273
|
+
* paramètre — une valeur ne peut donc pas devenir du code.
|
|
274
|
+
*/
|
|
275
|
+
export const databaseFilterOperatorSchema = z.enum([
|
|
276
|
+
'eq',
|
|
277
|
+
'ne',
|
|
278
|
+
'contains',
|
|
279
|
+
'starts',
|
|
280
|
+
'ends',
|
|
281
|
+
'gt',
|
|
282
|
+
'gte',
|
|
283
|
+
'lt',
|
|
284
|
+
'lte',
|
|
285
|
+
'isNull',
|
|
286
|
+
'notNull'
|
|
287
|
+
]);
|
|
288
|
+
export type DatabaseFilterOperator = z.infer<typeof databaseFilterOperatorSchema>;
|
|
289
|
+
|
|
290
|
+
/** Un critère de recherche. `value` est ignorée par `isNull` / `notNull`. */
|
|
291
|
+
export const databaseFilterSchema = z.object({
|
|
292
|
+
column: z.string().min(1).max(DATABASE_NAME_MAX_LENGTH),
|
|
293
|
+
operator: databaseFilterOperatorSchema,
|
|
294
|
+
value: z.string().max(1000)
|
|
295
|
+
});
|
|
296
|
+
export type DatabaseFilter = z.infer<typeof databaseFilterSchema>;
|
|
297
|
+
|
|
298
|
+
/** L'ordre d'affichage demandé, colonne validée contre la table réelle. */
|
|
299
|
+
export const databaseSortSchema = z.object({
|
|
300
|
+
column: z.string().min(1).max(DATABASE_NAME_MAX_LENGTH),
|
|
301
|
+
direction: z.enum(['asc', 'desc'])
|
|
302
|
+
});
|
|
303
|
+
export type DatabaseSort = z.infer<typeof databaseSortSchema>;
|
|
304
|
+
|
|
305
|
+
// ------------------------------------------------------- écriture de lignes
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* La valeur d'une colonne, à l'écriture.
|
|
309
|
+
*
|
|
310
|
+
* `null` est un vrai `NULL`, et non la chaîne vide : les deux se distinguent à
|
|
311
|
+
* la saisie, et les confondre viderait une colonne « non nulle » au lieu de
|
|
312
|
+
* refuser. Tout le reste voyage en chaîne, comme à la lecture — le moteur
|
|
313
|
+
* convertit, le paramètre étant lié.
|
|
314
|
+
*/
|
|
315
|
+
export const databaseCellSchema = z.object({
|
|
316
|
+
column: z.string().min(1).max(DATABASE_NAME_MAX_LENGTH),
|
|
317
|
+
value: z.string().max(65535).nullable()
|
|
318
|
+
});
|
|
319
|
+
export type DatabaseCell = z.infer<typeof databaseCellSchema>;
|
|
320
|
+
|
|
321
|
+
/** Ce qu'une instruction libre a produit : des lignes, ou un décompte. */
|
|
322
|
+
export const databaseExecutionSchema = z.object({
|
|
323
|
+
/** Le résultat d'une lecture ; `null` pour une écriture. */
|
|
324
|
+
rows: databaseRowsSchema.nullable(),
|
|
325
|
+
/** Le nombre de lignes touchées par une écriture ; `null` pour une lecture. */
|
|
326
|
+
affected: z.number().int().nonnegative().nullable(),
|
|
327
|
+
elapsedMs: z.number().int().nonnegative()
|
|
328
|
+
});
|
|
329
|
+
export type DatabaseExecution = z.infer<typeof databaseExecutionSchema>;
|
|
330
|
+
|
|
331
|
+
/** Les formats d'export proposés. */
|
|
332
|
+
export const databaseExportFormatSchema = z.enum(['csv', 'json', 'sql']);
|
|
333
|
+
export type DatabaseExportFormat = z.infer<typeof databaseExportFormatSchema>;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Une plage d'identifiants à exporter, bornes comprises.
|
|
337
|
+
*
|
|
338
|
+
* Deux nombres et non un fragment de texte : « 1-500, 900 » est une commodité de
|
|
339
|
+
* saisie, elle est analysée dans le navigateur et ne traverse jamais le contrat.
|
|
340
|
+
* Le serveur ne reçoit donc que des bornes, qu'il lie en paramètres — un
|
|
341
|
+
* identifiant saisi ne peut pas devenir du SQL. Une valeur seule s'écrit
|
|
342
|
+
* `{ from: n, to: n }`.
|
|
343
|
+
*/
|
|
344
|
+
export const databaseIdRangeSchema = z.object({
|
|
345
|
+
from: z.number().int(),
|
|
346
|
+
to: z.number().int()
|
|
347
|
+
});
|
|
348
|
+
export type DatabaseIdRange = z.infer<typeof databaseIdRangeSchema>;
|
|
349
|
+
|
|
350
|
+
/** Comment une mesure est comparée à son seuil. */
|
|
351
|
+
export const databaseComparatorSchema = z.enum(['gt', 'gte', 'lt', 'lte', 'eq', 'ne']);
|
|
352
|
+
export type DatabaseComparator = z.infer<typeof databaseComparatorSchema>;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Une condition d'alerte : une requête qui rend **un seul nombre**, comparée à
|
|
356
|
+
* un seuil.
|
|
357
|
+
*
|
|
358
|
+
* Un seul nombre, et c'est la contrainte qui rend le reste possible : un
|
|
359
|
+
* `SELECT COUNT(*) …` ou un `SELECT AVG(…) …` se compare, se raconte dans un
|
|
360
|
+
* message et se relit dans l'historique. Une requête qui rendrait un tableau
|
|
361
|
+
* n'aurait pas de vérité à confronter à un seuil.
|
|
362
|
+
*/
|
|
363
|
+
export const databaseConditionSchema = z.object({
|
|
364
|
+
sql: z.string().min(1).max(DATABASE_SQL_MAX_LENGTH),
|
|
365
|
+
comparator: databaseComparatorSchema,
|
|
366
|
+
threshold: z.number(),
|
|
367
|
+
/** Nom court de la mesure, repris dans le message ({@link databaseAlertSchema}). */
|
|
368
|
+
label: z.string().max(DATABASE_ALERT_NAME_MAX_LENGTH)
|
|
369
|
+
});
|
|
370
|
+
export type DatabaseCondition = z.infer<typeof databaseConditionSchema>;
|
|
371
|
+
|
|
372
|
+
/** Comment les conditions se combinent. */
|
|
373
|
+
export const databaseCombinatorSchema = z.enum(['and', 'or']);
|
|
374
|
+
export type DatabaseCombinator = z.infer<typeof databaseCombinatorSchema>;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Une alerte : des conditions, un opérateur qui les relie, un message.
|
|
378
|
+
*
|
|
379
|
+
* Évaluée par le relevé périodique, donc **seulement si celui-ci est actif** sur
|
|
380
|
+
* la base. Une alerte définie sur une base au repos est inerte, et l'interface
|
|
381
|
+
* le dit plutôt que de laisser croire à une surveillance qui n'existe pas.
|
|
382
|
+
*
|
|
383
|
+
* La notification part sur les canaux de l'espace — le compte mail et le webhook
|
|
384
|
+
* réglés dans Uptime — parce que ce sont les mêmes canaux pour les mêmes
|
|
385
|
+
* personnes, et qu'en avoir deux jeux à tenir à jour serait une source d'erreur
|
|
386
|
+
* de plus.
|
|
387
|
+
*/
|
|
388
|
+
export const databaseAlertSchema = z.object({
|
|
389
|
+
id: z.number().int().positive(),
|
|
390
|
+
databaseId: z.number().int().positive(),
|
|
391
|
+
name: z.string().max(DATABASE_ALERT_NAME_MAX_LENGTH),
|
|
392
|
+
enabled: z.boolean(),
|
|
393
|
+
combinator: databaseCombinatorSchema,
|
|
394
|
+
conditions: z.array(databaseConditionSchema),
|
|
395
|
+
/**
|
|
396
|
+
* Le message envoyé. `{label}` y est remplacé par la valeur mesurée de la
|
|
397
|
+
* condition portant ce nom — c'est ce qui permet d'écrire « déjà {erreurs}
|
|
398
|
+
* erreurs cette heure-ci » plutôt qu'un texte qui ne dit rien de la mesure.
|
|
399
|
+
*/
|
|
400
|
+
message: z.string().max(DATABASE_ALERT_MESSAGE_MAX_LENGTH),
|
|
401
|
+
/** L'alerte est-elle franchie **en ce moment** ? */
|
|
402
|
+
firing: z.boolean(),
|
|
403
|
+
/** Dernière évaluation, et dernier déclenchement (deux dates distinctes). */
|
|
404
|
+
lastCheckAt: z.number().int().nullable(),
|
|
405
|
+
lastFiredAt: z.number().int().nullable(),
|
|
406
|
+
/** Ce qu'a rendu la dernière évaluation, condition par condition. */
|
|
407
|
+
lastValues: z.array(z.number().nullable()),
|
|
408
|
+
/** Pourquoi la dernière évaluation a échoué, s'il y a lieu. */
|
|
409
|
+
lastError: z.string().nullable(),
|
|
410
|
+
created: z.number().int()
|
|
411
|
+
});
|
|
412
|
+
export type DatabaseAlert = z.infer<typeof databaseAlertSchema>;
|
|
413
|
+
|
|
414
|
+
// --------------------------------------------------------------- lignes SQL
|
|
415
|
+
|
|
416
|
+
/** Ligne SQL (serveur uniquement). */
|
|
417
|
+
export interface DatabaseRow {
|
|
418
|
+
id: number;
|
|
419
|
+
workspace_id: number;
|
|
420
|
+
engine: string;
|
|
421
|
+
/** Condensé du nom en minuscules : porte l'unicité dans l'espace. */
|
|
422
|
+
name_ref: string;
|
|
423
|
+
sort_order: number;
|
|
424
|
+
monitor_enabled: number;
|
|
425
|
+
interval_seconds: number;
|
|
426
|
+
last_check_at: number | null;
|
|
427
|
+
/** Durée du dernier relevé, en ms. Renseignée aussi sur un échec. */
|
|
428
|
+
last_elapsed_ms: number | null;
|
|
429
|
+
status: string;
|
|
430
|
+
last_error: string | null;
|
|
431
|
+
server_version: string | null;
|
|
432
|
+
size_bytes: number | null;
|
|
433
|
+
table_count: number | null;
|
|
434
|
+
/** { name, host, port, database, username } chiffré. */
|
|
435
|
+
content: string;
|
|
436
|
+
/** Mot de passe de la base, chiffré. Ne sort jamais du serveur. */
|
|
437
|
+
secret_enc: string | null;
|
|
438
|
+
/** { kind, host, port, username, auth } chiffré. */
|
|
439
|
+
access_content: string | null;
|
|
440
|
+
/** Mot de passe SSH ou clé privée, chiffré. Ne sort jamais du serveur. */
|
|
441
|
+
access_secret_enc: string | null;
|
|
442
|
+
created: number;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Ligne SQL (serveur uniquement). */
|
|
446
|
+
export interface DatabaseAlertRow {
|
|
447
|
+
id: number;
|
|
448
|
+
database_id: number;
|
|
449
|
+
workspace_id: number;
|
|
450
|
+
enabled: number;
|
|
451
|
+
combinator: string;
|
|
452
|
+
firing: number;
|
|
453
|
+
last_check_at: number | null;
|
|
454
|
+
last_fired_at: number | null;
|
|
455
|
+
last_error: string | null;
|
|
456
|
+
/** { name, conditions, message, lastValues } chiffré. */
|
|
457
|
+
content: string;
|
|
458
|
+
created: number;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Ligne SQL (serveur uniquement) : la liaison projet → base. */
|
|
462
|
+
export interface ProjectDatabaseLinkRow {
|
|
463
|
+
project_id: number;
|
|
464
|
+
database_id: number;
|
|
465
|
+
workspace_id: number;
|
|
466
|
+
created: number;
|
|
467
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Les cibles de déploiement d'un espace, et l'historique de ce qu'on y a poussé.
|
|
5
|
+
*
|
|
6
|
+
* **Une cible appartient à l'espace, pas à un projet.** C'est le renversement
|
|
7
|
+
* qui fonde cette feature, le même que celui des dépôts git : une pile compose
|
|
8
|
+
* sert souvent deux projets (un client, un serveur), et la modéliser comme une
|
|
9
|
+
* propriété de l'un d'eux interdisait à l'autre de la voir. Un projet n'en garde
|
|
10
|
+
* donc qu'une **liaison** (`project_deploy_links`), et délier n'efface jamais la
|
|
11
|
+
* cible.
|
|
12
|
+
*
|
|
13
|
+
* Portée volontairement **étroite** : déclencher et suivre. DevEye ne configure
|
|
14
|
+
* pas le déploiement, ne gère ni domaines ni variables d'environnement — tout
|
|
15
|
+
* cela vit chez le fournisseur, qui le fait mieux. Ce module répond à deux
|
|
16
|
+
* questions : « est-ce que je peux lancer ça d'ici ? » et « où en est le
|
|
17
|
+
* dernier ? ».
|
|
18
|
+
*
|
|
19
|
+
* **Toujours à l'étage ouvert**, quel que soit le tier des projets qui s'y
|
|
20
|
+
* rattachent : le suivi d'état tourne sans session, et une cible d'espace ne
|
|
21
|
+
* peut pas suivre le palier de confidentialité de l'un de ses projets.
|
|
22
|
+
* Corollaire assumé, identique à celui des dépôts : un projet confidentiel n'a
|
|
23
|
+
* pas de déploiement.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const DEPLOY_TITLE_MAX_LENGTH = 120;
|
|
27
|
+
export const DEPLOY_DESCRIPTION_MAX_LENGTH = 500;
|
|
28
|
+
export const DEPLOY_TARGET_NAME_MAX_LENGTH = 120;
|
|
29
|
+
export const DEPLOY_EXTERNAL_ID_MAX_LENGTH = 128;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Les fournisseurs que le module sait déclencher.
|
|
33
|
+
*
|
|
34
|
+
* Un seul pour l'instant, et l'énumération est là pour que le second n'ait pas à
|
|
35
|
+
* réécrire le contrat. Distinct de `credentialProviderSchema`, qui couvre aussi
|
|
36
|
+
* GitHub : un jeton GitHub ne déploie rien.
|
|
37
|
+
*/
|
|
38
|
+
export const deployProviderSchema = z.enum(['dokploy']);
|
|
39
|
+
export type DeployProvider = z.infer<typeof deployProviderSchema>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* L'état d'un déploiement, ramené à quatre valeurs.
|
|
43
|
+
*
|
|
44
|
+
* Les fournisseurs ont chacun leur vocabulaire (`done`, `success`, `idle`,
|
|
45
|
+
* `error`, `failed`…). L'adaptateur les projette là-dessus, pour que
|
|
46
|
+
* l'interface n'ait qu'un seul jeu d'états à connaître.
|
|
47
|
+
*/
|
|
48
|
+
export const deployStatusSchema = z.enum(['queued', 'running', 'success', 'failed']);
|
|
49
|
+
export type DeployStatus = z.infer<typeof deployStatusSchema>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ce qu'on déploie chez Dokploy : une **application** ou une pile **compose**.
|
|
53
|
+
*
|
|
54
|
+
* La distinction n'est pas cosmétique — chaque type a sa propre procédure de
|
|
55
|
+
* déclenchement (`application.deploy` / `compose.deploy`) et sa propre
|
|
56
|
+
* procédure d'historique (`deployment.all` / `deployment.allByCompose`). Une
|
|
57
|
+
* cible sans son type serait indéployable.
|
|
58
|
+
*
|
|
59
|
+
* En pratique, une infra Dokploy est souvent majoritairement composée de piles
|
|
60
|
+
* compose : les ignorer reviendrait à ne rien pouvoir déployer.
|
|
61
|
+
*/
|
|
62
|
+
export const deployTargetKindSchema = z.enum(['application', 'compose']);
|
|
63
|
+
export type DeployTargetKind = z.infer<typeof deployTargetKindSchema>;
|
|
64
|
+
|
|
65
|
+
/** Une cible de l'espace, et l'état de son dernier déclenchement. */
|
|
66
|
+
export const deployTargetSchema = z.object({
|
|
67
|
+
id: z.number().int().positive(),
|
|
68
|
+
provider: deployProviderSchema,
|
|
69
|
+
kind: deployTargetKindSchema,
|
|
70
|
+
/** Identifiant de la cible chez le fournisseur. En clair : il porte l'unicité. */
|
|
71
|
+
externalId: z.string().max(DEPLOY_EXTERNAL_ID_MAX_LENGTH),
|
|
72
|
+
name: z.string().max(DEPLOY_TARGET_NAME_MAX_LENGTH),
|
|
73
|
+
/** `null` = le jeton a été retiré ; la cible reste, indéployable, et le dit. */
|
|
74
|
+
credentialId: z.number().int().positive().nullable(),
|
|
75
|
+
/**
|
|
76
|
+
* L'adresse de l'instance qui l'héberge, recopiée du jeton.
|
|
77
|
+
*
|
|
78
|
+
* Deux instances Dokploy peuvent servir la même pile sous le même nom ; sans
|
|
79
|
+
* elle, deux lignes de la liste seraient indiscernables. Elle vient du jeton
|
|
80
|
+
* et n'est jamais saisie ici.
|
|
81
|
+
*/
|
|
82
|
+
baseUrl: z.string().nullable(),
|
|
83
|
+
/** L'état du dernier déploiement, ou `null` si rien n'est jamais parti d'ici. */
|
|
84
|
+
lastStatus: deployStatusSchema.nullable(),
|
|
85
|
+
lastDeployAt: z.number().int().nullable(),
|
|
86
|
+
/**
|
|
87
|
+
* Cet élément vient d'un **autre espace**, qui le projette ici.
|
|
88
|
+
*
|
|
89
|
+
* L'écran le signale d'une pastille : sans elle, rien ne distingue une
|
|
90
|
+
* ligne locale d'une fenêtre sur l'espace voisin — et les gestes réservés
|
|
91
|
+
* au domicile (supprimer, re-partager) sembleraient cassés au lieu de
|
|
92
|
+
* s'expliquer.
|
|
93
|
+
*/
|
|
94
|
+
foreign: z.boolean(),
|
|
95
|
+
/** Combien de projets la déploient. */
|
|
96
|
+
projectCount: z.number().int().nonnegative(),
|
|
97
|
+
created: z.number().int()
|
|
98
|
+
});
|
|
99
|
+
export type DeployTarget = z.infer<typeof deployTargetSchema>;
|
|
100
|
+
|
|
101
|
+
/** Une cible proposée au choix, telle que le fournisseur la déclare. */
|
|
102
|
+
export const deployCandidateSchema = z.object({
|
|
103
|
+
kind: deployTargetKindSchema,
|
|
104
|
+
externalId: z.string(),
|
|
105
|
+
name: z.string(),
|
|
106
|
+
/** Chemin lisible chez le fournisseur (projet / environnement), s'il en donne un. */
|
|
107
|
+
path: z.string().nullable()
|
|
108
|
+
});
|
|
109
|
+
export type DeployCandidate = z.infer<typeof deployCandidateSchema>;
|
|
110
|
+
|
|
111
|
+
/** Un déclenchement, et ce qu'il est devenu. */
|
|
112
|
+
export const deploymentSchema = z.object({
|
|
113
|
+
id: z.number().int().positive(),
|
|
114
|
+
targetId: z.number().int().positive(),
|
|
115
|
+
/** Identifiant chez le fournisseur, quand il en donne un au déclenchement. */
|
|
116
|
+
externalId: z.string().nullable(),
|
|
117
|
+
status: deployStatusSchema,
|
|
118
|
+
/** Qui l'a déclenché ; `null` = tâche de fond, ou compte supprimé depuis. */
|
|
119
|
+
triggeredByUserId: z.number().int().positive().nullable(),
|
|
120
|
+
title: z.string(),
|
|
121
|
+
description: z.string(),
|
|
122
|
+
url: z.string().nullable(),
|
|
123
|
+
startedAt: z.number().int(),
|
|
124
|
+
finishedAt: z.number().int().nullable()
|
|
125
|
+
});
|
|
126
|
+
export type Deployment = z.infer<typeof deploymentSchema>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Une ligne d'historique telle que le fournisseur la connaît — pas seulement
|
|
130
|
+
* ce que DevEye a déclenché.
|
|
131
|
+
*
|
|
132
|
+
* `deploymentSchema` porte l'identité DevEye d'un déclenchement (`id`, `targetId`,
|
|
133
|
+
* `triggeredByUserId`) ; celui-ci n'a que ce que Dokploy rend, y compris pour ce
|
|
134
|
+
* qui est parti de sa propre interface ou d'une CI. Aucun `id` DevEye n'existe
|
|
135
|
+
* pour ces lignes-là, d'où un schéma distinct plutôt qu'un `Deployment` aux
|
|
136
|
+
* champs devinés.
|
|
137
|
+
*/
|
|
138
|
+
export const deployHistoryEntrySchema = z.object({
|
|
139
|
+
externalId: z.string().nullable(),
|
|
140
|
+
status: deployStatusSchema,
|
|
141
|
+
title: z.string(),
|
|
142
|
+
description: z.string(),
|
|
143
|
+
startedAt: z.number().int(),
|
|
144
|
+
finishedAt: z.number().int().nullable()
|
|
145
|
+
});
|
|
146
|
+
export type DeployHistoryEntry = z.infer<typeof deployHistoryEntrySchema>;
|
|
147
|
+
|
|
148
|
+
/** Ligne SQL (serveur uniquement). */
|
|
149
|
+
export interface DeployTargetRow {
|
|
150
|
+
id: number;
|
|
151
|
+
workspace_id: number;
|
|
152
|
+
credential_id: number | null;
|
|
153
|
+
provider: string;
|
|
154
|
+
/** 'application' | 'compose'. */
|
|
155
|
+
target_kind: string;
|
|
156
|
+
external_id: string;
|
|
157
|
+
/** Rang dans la liste, entièrement défini par l'utilisateur (`deploy.reorder`). */
|
|
158
|
+
sort_order: number;
|
|
159
|
+
content: string;
|
|
160
|
+
/**
|
|
161
|
+
* Dernier rapprochement réussi avec le fournisseur ; `null` = jamais.
|
|
162
|
+
*
|
|
163
|
+
* Porte deux rôles à la fois, et c'est voulu : il ordonne les cibles à
|
|
164
|
+
* réinterroger (la plus ancienne d'abord), **et** il distingue le premier
|
|
165
|
+
* rapprochement des suivants. Cette seconde lecture est ce qui empêche
|
|
166
|
+
* l'import initial de notifier : la première fois, tout l'historique de la
|
|
167
|
+
* cible est « nouveau » sans que rien ne vienne de se produire.
|
|
168
|
+
*/
|
|
169
|
+
synced_at: number | null;
|
|
170
|
+
created: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* La même, augmentée de ce qu'une liste montre sans ouvrir la fiche : combien de
|
|
175
|
+
* projets s'en servent, et où en est le dernier déploiement. Calculé par
|
|
176
|
+
* jointure plutôt que recopié dans des colonnes, qui dériveraient.
|
|
177
|
+
*/
|
|
178
|
+
export interface DeployTargetWithUsageRow extends DeployTargetRow {
|
|
179
|
+
base_url: string | null;
|
|
180
|
+
project_count: number;
|
|
181
|
+
last_status: string | null;
|
|
182
|
+
last_deploy_at: number | null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Ligne SQL (serveur uniquement). */
|
|
186
|
+
export interface DeploymentRow {
|
|
187
|
+
id: number;
|
|
188
|
+
target_id: number;
|
|
189
|
+
workspace_id: number;
|
|
190
|
+
external_id: string | null;
|
|
191
|
+
status: string;
|
|
192
|
+
triggered_by_user_id: number | null;
|
|
193
|
+
started_at: number;
|
|
194
|
+
finished_at: number | null;
|
|
195
|
+
/**
|
|
196
|
+
* Un avis est-il déjà parti pour ce déploiement ?
|
|
197
|
+
*
|
|
198
|
+
* Même rôle que `uptime_incidents.notified`, et pour la même raison : un avis
|
|
199
|
+
* appartient au **déploiement**, pas au tour de sondage qui l'a vu. Sans
|
|
200
|
+
* cette colonne, chaque tour renotifierait le même échec — et l'import
|
|
201
|
+
* initial d'une cible en enverrait un par ligne d'historique.
|
|
202
|
+
*/
|
|
203
|
+
notified: number;
|
|
204
|
+
content: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Une cible à réinterroger, avec l'adresse de son instance et de quoi choisir
|
|
209
|
+
* son tour.
|
|
210
|
+
*
|
|
211
|
+
* `base_url` vient du jeton par jointure : la boucle de fond n'a pas de session
|
|
212
|
+
* pour repasser par la feature, et faire un second aller-retour par cible pour
|
|
213
|
+
* lire son jeton coûterait une requête de plus pour une donnée déjà jointe.
|
|
214
|
+
*
|
|
215
|
+
* `in_flight` compte les déploiements non terminés que DevEye connaît. Il ne
|
|
216
|
+
* sert qu'à trier : une cible qui a quelque chose en vol passe à chaque tour,
|
|
217
|
+
* les autres attendent leur cadence. C'est ce qui permet de suivre un
|
|
218
|
+
* déploiement à la minute sans sonder toutes les cibles aussi souvent.
|
|
219
|
+
*/
|
|
220
|
+
export interface DeployTargetSyncRow extends DeployTargetRow {
|
|
221
|
+
base_url: string | null;
|
|
222
|
+
in_flight: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Ligne SQL (serveur uniquement) : la liaison projet → cible. */
|
|
226
|
+
export interface ProjectDeployLinkRow {
|
|
227
|
+
project_id: number;
|
|
228
|
+
target_id: number;
|
|
229
|
+
workspace_id: number;
|
|
230
|
+
created: number;
|
|
231
|
+
}
|