[FEAT] rework the generation of code for typescript (#27)

This PR is not compatible with previous code...
Rename some Annotation to have a better experience

Update the generation of the Typescript object 
===================================

By default the generation of object are only the object requested

if you annotate the object with: `@ApiGenerationMode(create = true,
update = true)`

the generation will add 2 object in typescript:
  - `MyClassUpdate` that contain the object specific for `PUT` request
  - `MyClassCreate` that contain the object specific for `POST` request
  - the PATCH request are generate with `Partial<MyClassUpdate>`

If you do not generate the create or update the system will wrap the
bass by default: `MyClass`

Add support of hidding the element in the generated object
=============================================

When generate the object some field are not needed to transmit to the
client, then we add `@ApiAccessLimitation(creatable = false, updatable =
false)` that permit to remove the field when Update or Create object are
generated

It will be used to check the Input validity instead of
`@schema(readonly)` (error of implementation)


TODO:
=====

  - Some issue when request Update generation and parent does not exist
- The checker is not updated with the use of the `@ApiAccessLimitation`
decorator.


dependencies:
===========
  - Closes: https://github.com/kangaroo-and-rabbit/archidata/issues/22
This commit is contained in:
Edouard DUPIN 2025-03-08 14:32:16 +01:00 committed by GitHub
parent 2174d7689f
commit c1ccaf20ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 692 additions and 328 deletions

View File

@ -2,6 +2,7 @@ package org.kar.archidata.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
@ -41,6 +42,24 @@ import jakarta.ws.rs.DefaultValue;
public class AnnotationTools {
static final Logger LOGGER = LoggerFactory.getLogger(AnnotationTools.class);
public static <TYPE extends Annotation> TYPE get(final Parameter param, final Class<TYPE> clazz) {
final TYPE[] annotations = param.getDeclaredAnnotationsByType(clazz);
if (annotations.length == 0) {
return null;
}
return annotations[0];
}
public static <TYPE extends Annotation> TYPE[] gets(final Parameter param, final Class<TYPE> clazz) {
final TYPE[] annotations = param.getDeclaredAnnotationsByType(clazz);
if (annotations.length == 0) {
return null;
}
return annotations;
}
public static <TYPE extends Annotation> TYPE get(final Field element, final Class<TYPE> clazz) {
final TYPE[] annotations = element.getDeclaredAnnotationsByType(clazz);
@ -59,6 +78,24 @@ public class AnnotationTools {
return annotations;
}
public static <TYPE extends Annotation> TYPE get(final Class<?> classObject, final Class<TYPE> clazz) {
final TYPE[] annotations = classObject.getDeclaredAnnotationsByType(clazz);
if (annotations.length == 0) {
return null;
}
return annotations[0];
}
public static <TYPE extends Annotation> TYPE[] gets(final Class<?> classObject, final Class<TYPE> clazz) {
final TYPE[] annotations = classObject.getDeclaredAnnotationsByType(clazz);
if (annotations.length == 0) {
return null;
}
return annotations;
}
// For SQL declaration table Name
public static String getTableName(final Class<?> clazz, final QueryOptions options) throws DataAccessException {
if (options != null) {
@ -121,14 +158,6 @@ public class AnnotationTools {
return get(element, CollectionNotEmpty.class);
}
public static boolean getSchemaReadOnly(final Field element) {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Schema.class);
if (annotation.length == 0) {
return false;
}
return ((Schema) annotation[0]).readOnly();
}
public static String getSchemaExample(final Class<?> element) {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Schema.class);
if (annotation.length == 0) {
@ -137,14 +166,6 @@ public class AnnotationTools {
return ((Schema) annotation[0]).example();
}
public static boolean getNoWriteSpecificMode(final Class<?> element) {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(NoWriteSpecificMode.class);
if (annotation.length == 0) {
return false;
}
return true;
}
public static String getSchemaDescription(final Class<?> element) {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Schema.class);
if (annotation.length == 0) {

View File

@ -1,41 +0,0 @@
package org.kar.archidata.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The NoWriteSpecificMode annotation is used to indicate that there is no
* specific API for write mode when generating code for other languages. This
* annotation is particularly useful in code generators for client libraries
* where a separate, reduced data structure for write operations is not needed.
*
* <p>Usage:
* - Target: This annotation can be applied to class types.
* - Retention: The annotation is retained at runtime, allowing it to be
* processed by frameworks or libraries that handle code generation logic.
*
* <p>Behavior:
* - When applied to a class, the NoWriteSpecificMode annotation specifies
* that the class does not require a separate API or data structure for
* write operations. This can simplify the generated code by avoiding the
* creation of redundant structures.
*
* <p>Example:
* <pre>{@code
* @NoWriteSpecificMode
* public class User {
* public String username;
* public String email;
* }
* }</pre>
*
* In this example, the User class will not generate a separate API or data
* structure for write operations in the client code.
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface NoWriteSpecificMode {
}

View File

@ -0,0 +1,21 @@
package org.kar.archidata.annotation.apiGenerator;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(FIELD)
public @interface ApiAccessLimitation {
/**
* (Optional) The field is accessible in creation (POST)
*/
boolean creatable() default true;
/**
* (Optional) The field is accessible in update mode (PUT, PATCH)
*/
boolean updatable() default true;
}

View File

@ -1,4 +1,4 @@
package org.kar.archidata.annotation;
package org.kar.archidata.annotation.apiGenerator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@ -8,7 +8,7 @@ import java.lang.annotation.Target;
/** In case of the update parameter with String input to detect null element. */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface AsyncType {
public @interface ApiAsyncType {
// Possible class values.
Class<?>[] value();

View File

@ -0,0 +1,57 @@
package org.kar.archidata.annotation.apiGenerator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The ApiGenerationMode annotation is used to indicate the generation mode for
* API operations when producing code for other languages. This annotation is
* particularly useful in code generators for client libraries where specific
* data structures for read, create, and update operations may or may not be needed.
*
* <p>Usage:
* - Target: This annotation can be applied to class types.
* - Retention: The annotation is retained at runtime, allowing it to be
* processed by frameworks or libraries that handle code generation logic.
*
* <p>Behavior:
* - When applied to a class, the ApiGenerationMode annotation specifies
* which API operations (read, create, update) should generate specific
* data structures. This can simplify the generated code by avoiding the
* creation of unnecessary structures.
*
* <p>Example:
* <pre>{@code
* @ApiGenerationMode(creatable=false, updatable=false)
* public class User {
* public String username;
* public String email;
* }
* }</pre>
*
* In this example, the User class will not generate separate data structures
* for create and update operations in the client code, only for read operations.
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiGenerationMode {
/**
* (Optional) Enable the generation of specific code for read access
* (generate object: MyClass).
*/
boolean read() default true;
/**
* (Optional) Enable the generation of specific code for create access
* (generate object: MyClassCreate).
*/
boolean create() default false;
/**
* (Optional) Enable the generation of specific code for update access
* (generate object: MyClassUpdate).
*/
boolean update() default false;
}

View File

@ -1,4 +1,4 @@
package org.kar.archidata.annotation;
package org.kar.archidata.annotation.apiGenerator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@ -32,9 +32,9 @@ import java.lang.annotation.Target;
* @Operation(description = "Add a cover on a specific album")
* @TypeScriptProgress
* public Album uploadCover(@PathParam("id") final Long id,
* @FormDataOptional @FormDataParam("uri") final String uri,
* @FormDataOptional @FormDataParam("file") final InputStream fileInputStream,
* @FormDataOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData)
* @ApiInputOptional @FormDataParam("uri") final String uri,
* @ApiInputOptional @FormDataParam("file") final InputStream fileInputStream,
* @ApiInputOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData)
* throws Exception {
* // some code
* }
@ -71,6 +71,6 @@ import java.lang.annotation.Target;
*/
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface FormDataOptional {
public @interface ApiInputOptional {
}

View File

@ -1,4 +1,4 @@
package org.kar.archidata.annotation;
package org.kar.archidata.annotation.apiGenerator;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@ -68,4 +68,4 @@ import java.lang.annotation.Target;
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeScriptProgress {}
public @interface ApiTypeScriptProgress {}

View File

@ -24,6 +24,7 @@ import javax.imageio.ImageIO;
import org.bson.types.ObjectId;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.apiGenerator.ApiInputOptional;
import org.kar.archidata.annotation.security.PermitTokenInURI;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.QueryCondition;
@ -426,7 +427,7 @@ public class DataResource {
public Response retrieveDataFull(
@Context final SecurityContext sc,
@QueryParam(HttpHeaders.AUTHORIZATION) final String token,
@HeaderParam("Range") final String range,
@ApiInputOptional @HeaderParam("Range") final String range,
@PathParam("oid") final ObjectId oid,
@PathParam("name") final String name) throws Exception {
final GenericContext gc = (GenericContext) sc.getUserPrincipal();

View File

@ -3,13 +3,13 @@ package org.kar.archidata.catcher;
import java.time.Instant;
import org.bson.types.ObjectId;
import org.kar.archidata.annotation.NoWriteSpecificMode;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import jakarta.persistence.Column;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.core.Response;
@NoWriteSpecificMode
@ApiGenerationMode
public class RestErrorResponse {
public ObjectId oid = new ObjectId();
@NotNull

View File

@ -151,13 +151,6 @@ public class DotClassElement {
return ".optional()";
}
public String readOnlyZod(final FieldProperty field) {
if (field.readOnly()) {
return ".readonly()";
}
return "";
}
public String generateBaseObject() {
final StringBuilder out = new StringBuilder();
return out.toString();

View File

@ -9,9 +9,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.kar.archidata.annotation.AnnotationTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.core.Context;
public class ApiModel {
static final Logger LOGGER = LoggerFactory.getLogger(ApiModel.class);
@ -37,6 +41,8 @@ public class ApiModel {
public String name;
// list of all parameters (/{key}/...
public final Map<String, List<ClassModel>> parameters = new HashMap<>();
// list of all headers of the request (/{key}/...
public final Map<String, OptionalClassModel> headers = new HashMap<>();
// list of all query (?key...)
public final Map<String, List<ClassModel>> queries = new HashMap<>();
// when request multi-part, need to separate it.
@ -153,11 +159,12 @@ public class ApiModel {
} else {
parameterModel.add(previousModel.add(parameterType));
}
final Context contextAnnotation = AnnotationTools.get(parameter, Context.class);
final HeaderParam headerParam = AnnotationTools.get(parameter, HeaderParam.class);
final String pathParam = ApiTool.apiAnnotationGetPathParam(parameter);
final String queryParam = ApiTool.apiAnnotationGetQueryParam(parameter);
final String formDataParam = ApiTool.apiAnnotationGetFormDataParam(parameter);
final boolean formDataParamOptional = ApiTool.apiAnnotationGetFormDataOptional(parameter);
final boolean apiInputOptional = ApiTool.apiAnnotationGetApiInputOptional(parameter);
if (queryParam != null) {
if (!this.queries.containsKey(queryParam)) {
this.queries.put(queryParam, parameterModel);
@ -169,7 +176,13 @@ public class ApiModel {
} else if (formDataParam != null) {
if (!this.multiPartParameters.containsKey(formDataParam)) {
this.multiPartParameters.put(formDataParam,
new OptionalClassModel(parameterModel, formDataParamOptional));
new OptionalClassModel(parameterModel, apiInputOptional));
}
} else if (contextAnnotation != null) {
// out of scope parameters
} else if (headerParam != null) {
if (!this.headers.containsKey(headerParam.value())) {
this.headers.put(headerParam.value(), new OptionalClassModel(parameterModel, apiInputOptional));
}
} else {
this.unnamedElement.addAll(parameterModel);

View File

@ -7,9 +7,9 @@ import java.util.Arrays;
import java.util.List;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.FormDataOptional;
import org.kar.archidata.annotation.TypeScriptProgress;
import org.kar.archidata.annotation.apiGenerator.ApiInputOptional;
import org.kar.archidata.annotation.apiGenerator.ApiAsyncType;
import org.kar.archidata.annotation.apiGenerator.ApiTypeScriptProgress;
import org.kar.archidata.annotation.method.ARCHIVE;
import org.kar.archidata.annotation.method.RESTORE;
@ -53,7 +53,7 @@ public class ApiTool {
}
public static boolean apiAnnotationTypeScriptProgress(final Method element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(TypeScriptProgress.class);
final Annotation[] annotation = element.getDeclaredAnnotationsByType(ApiTypeScriptProgress.class);
if (annotation.length == 0) {
return false;
}
@ -159,8 +159,8 @@ public class ApiTool {
return ((QueryParam) annotation[0]).value();
}
public static boolean apiAnnotationGetFormDataOptional(final Parameter element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(FormDataOptional.class);
public static boolean apiAnnotationGetApiInputOptional(final Parameter element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(ApiInputOptional.class);
if (annotation.length == 0) {
return false;
}
@ -176,19 +176,19 @@ public class ApiTool {
}
public static Class<?>[] apiAnnotationGetAsyncType(final Parameter element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(AsyncType.class);
final Annotation[] annotation = element.getDeclaredAnnotationsByType(ApiAsyncType.class);
if (annotation.length == 0) {
return null;
}
return ((AsyncType) annotation[0]).value();
return ((ApiAsyncType) annotation[0]).value();
}
public static Class<?>[] apiAnnotationGetAsyncType(final Method element) throws Exception {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(AsyncType.class);
final Annotation[] annotation = element.getDeclaredAnnotationsByType(ApiAsyncType.class);
if (annotation.length == 0) {
return null;
}
return ((AsyncType) annotation[0]).value();
return ((ApiAsyncType) annotation[0]).value();
}
public static List<String> apiAnnotationGetConsumes(final Method element) throws Exception {

View File

@ -6,11 +6,14 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class ClassEnumModel extends ClassModel {
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.tools.AnnotationCreator;
public class ClassEnumModel extends ClassModel {
protected ClassEnumModel(final Class<?> clazz) {
this.originClasses = clazz;
this.noWriteSpecificMode = true;
this.apiGenerationMode = AnnotationCreator.createAnnotation(ApiGenerationMode.class, "readable", true,
"creatable", false, "updatable", false);
}
@Override

View File

@ -8,18 +8,24 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.tools.AnnotationCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class ClassModel {
private static final Logger LOGGER = LoggerFactory.getLogger(ClassModel.class);
protected boolean analyzeDone = false;
protected Class<?> originClasses = null;
protected boolean noWriteSpecificMode = false;
protected ApiGenerationMode apiGenerationMode = AnnotationCreator.createAnnotation(ApiGenerationMode.class);
protected List<ClassModel> dependencyModels = new ArrayList<>();
public Class<?> getOriginClasses() {
return this.originClasses;
}
public boolean isNoWriteSpecificMode() {
return this.noWriteSpecificMode;
public ApiGenerationMode getApiGenerationMode() {
return this.apiGenerationMode;
}
protected boolean isCompatible(final Class<?> clazz) {
@ -76,7 +82,15 @@ public abstract class ClassModel {
public abstract Set<ClassModel> getAlls();
public List<String> getReadOnlyField() {
public List<String> getReadOnlyFields() {
return List.of();
}
public List<String> getCreateFields() {
return List.of();
}
public List<String> getUpdateFields() {
return List.of();
}

View File

@ -11,7 +11,10 @@ import java.util.List;
import java.util.Set;
import org.kar.archidata.annotation.AnnotationTools;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.exception.DataAccessException;
import org.kar.archidata.tools.AnnotationCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,6 +34,10 @@ public class ClassObjectModel extends ClassModel {
public ClassObjectModel(final Class<?> clazz) {
this.originClasses = clazz;
final ApiGenerationMode tmp = AnnotationTools.get(clazz, ApiGenerationMode.class);
if (tmp != null) {
this.apiGenerationMode = tmp;
}
}
@Override
@ -74,15 +81,16 @@ public class ClassObjectModel extends ClassModel {
DecimalMax decimalMax,
Pattern pattern,
Email email,
Boolean readOnly,
ApiAccessLimitation accessLimitation,
Boolean notNull,
Boolean columnNotNull,
Boolean nullable) {
public FieldProperty(final String name, final ClassModel model, final ClassModel linkClass,
final String comment, final Size stringSize, final Min min, final Max max, final DecimalMin decimalMin,
final DecimalMax decimalMax, final Pattern pattern, final Email email, final Boolean readOnly,
final Boolean notNull, final Boolean columnNotNull, final Boolean nullable) {
final DecimalMax decimalMax, final Pattern pattern, final Email email,
final ApiAccessLimitation accessLimitation, final Boolean notNull, final Boolean columnNotNull,
final Boolean nullable) {
this.name = name;
this.model = model;
this.linkClass = linkClass;
@ -94,7 +102,11 @@ public class ClassObjectModel extends ClassModel {
this.email = email;
this.min = min;
this.max = max;
this.readOnly = readOnly;
if (accessLimitation == null) {
this.accessLimitation = AnnotationCreator.createAnnotation(ApiAccessLimitation.class);
} else {
this.accessLimitation = accessLimitation;
}
this.notNull = notNull;
this.columnNotNull = columnNotNull;
this.nullable = nullable;
@ -146,7 +158,7 @@ public class ClassObjectModel extends ClassModel {
AnnotationTools.getConstraintsDecimalMax(field), //
AnnotationTools.getConstraintsPattern(field), //
AnnotationTools.getConstraintsEmail(field), //
AnnotationTools.getSchemaReadOnly(field), //
AnnotationTools.get(field, ApiAccessLimitation.class), //
AnnotationTools.getConstraintsNotNull(field), //
AnnotationTools.getColumnNotNull(field), //
AnnotationTools.getNullable(field));
@ -192,7 +204,6 @@ public class ClassObjectModel extends ClassModel {
}
this.analyzeDone = true;
final Class<?> clazz = this.originClasses;
this.noWriteSpecificMode = AnnotationTools.getNoWriteSpecificMode(clazz);
this.isPrimitive = clazz.isPrimitive();
if (this.isPrimitive) {
return;
@ -259,15 +270,43 @@ public class ClassObjectModel extends ClassModel {
}
@Override
public List<String> getReadOnlyField() {
public List<String> getReadOnlyFields() {
final List<String> out = new ArrayList<>();
for (final FieldProperty field : this.fields) {
if (field.readOnly()) {
if (!field.accessLimitation().creatable() && !field.accessLimitation().updatable()) {
out.add(field.name);
}
}
if (this.extendsClass != null) {
out.addAll(this.extendsClass.getReadOnlyField());
out.addAll(this.extendsClass.getReadOnlyFields());
}
return out;
}
@Override
public List<String> getCreateFields() {
final List<String> out = new ArrayList<>();
for (final FieldProperty field : this.fields) {
if (field.accessLimitation().creatable()) {
out.add(field.name);
}
}
if (this.extendsClass != null) {
out.addAll(this.extendsClass.getReadOnlyFields());
}
return out;
}
@Override
public List<String> getUpdateFields() {
final List<String> out = new ArrayList<>();
for (final FieldProperty field : this.fields) {
if (field.accessLimitation().updatable()) {
out.add(field.name);
}
}
if (this.extendsClass != null) {
out.addAll(this.extendsClass.getReadOnlyFields());
}
return out;
}

View File

@ -45,7 +45,9 @@ public class TsApiGeneration {
final ClassEnumModel model,
final TsClassElementGroup tsGroup,
final Set<ClassModel> imports,
final Set<ClassModel> importWrite) throws IOException {
final Set<ClassModel> importUpdate,
final Set<ClassModel> importCreate,
final boolean partialObject) throws IOException {
imports.add(model);
final TsClassElement tsModel = tsGroup.find(model);
return tsModel.tsTypeName;
@ -55,34 +57,47 @@ public class TsApiGeneration {
final ClassObjectModel model,
final TsClassElementGroup tsGroup,
final Set<ClassModel> imports,
final Set<ClassModel> importWrite) throws IOException {
final Set<ClassModel> importUpdate,
final Set<ClassModel> importCreate,
final boolean partialObject) throws IOException {
final TsClassElement tsModel = tsGroup.find(model);
if (tsModel.nativeType != DefinedPosition.NATIVE) {
if (importWrite == null || tsModel.models.get(0).isNoWriteSpecificMode()) {
imports.add(model);
if (importCreate != null && tsModel.models.get(0).getApiGenerationMode().create()) {
importCreate.add(model);
} else if (importUpdate != null && tsModel.models.get(0).getApiGenerationMode().update()) {
importUpdate.add(model);
} else {
importWrite.add(model);
imports.add(model);
}
}
String out = tsModel.tsTypeName;
if (tsModel.nativeType != DefinedPosition.NORMAL) {
return tsModel.tsTypeName;
out = tsModel.tsTypeName;
} else if (importCreate != null && tsModel.models.get(0).getApiGenerationMode().create()) {
out = tsModel.tsTypeName + TsClassElement.MODEL_TYPE_CREATE;
} else if (importUpdate != null && tsModel.models.get(0).getApiGenerationMode().update()) {
out = tsModel.tsTypeName + TsClassElement.MODEL_TYPE_UPDATE;
}
if (importWrite != null && !tsModel.models.get(0).isNoWriteSpecificMode()) {
return tsModel.tsTypeName + "Write";
if (partialObject) {
return "Partial<" + out + ">";
}
return tsModel.tsTypeName;
return out;
}
public static String generateClassMapModelTypescript(
final ClassMapModel model,
final TsClassElementGroup tsGroup,
final Set<ClassModel> imports,
final Set<ClassModel> importWrite) throws IOException {
final Set<ClassModel> importUpdate,
final Set<ClassModel> importCreate,
final boolean partialObject) throws IOException {
final StringBuilder out = new StringBuilder();
out.append("{[key: ");
out.append(generateClassModelTypescript(model.keyModel, tsGroup, imports, importWrite));
out.append(generateClassModelTypescript(model.keyModel, tsGroup, imports, importUpdate, importCreate,
partialObject));
out.append("]: ");
out.append(generateClassModelTypescript(model.valueModel, tsGroup, imports, importWrite));
out.append(generateClassModelTypescript(model.valueModel, tsGroup, imports, importUpdate, importCreate,
partialObject));
out.append(";}");
return out.toString();
}
@ -91,9 +106,12 @@ public class TsApiGeneration {
final ClassListModel model,
final TsClassElementGroup tsGroup,
final Set<ClassModel> imports,
final Set<ClassModel> importWrite) throws IOException {
final Set<ClassModel> importUpdate,
final Set<ClassModel> importCreate,
final boolean partialObject) throws IOException {
final StringBuilder out = new StringBuilder();
out.append(generateClassModelTypescript(model.valueModel, tsGroup, imports, importWrite));
out.append(generateClassModelTypescript(model.valueModel, tsGroup, imports, importUpdate, importCreate,
partialObject));
out.append("[]");
return out.toString();
}
@ -102,18 +120,24 @@ public class TsApiGeneration {
final ClassModel model,
final TsClassElementGroup tsGroup,
final Set<ClassModel> imports,
final Set<ClassModel> importWrite) throws IOException {
final Set<ClassModel> importUpdate,
final Set<ClassModel> importCreate,
final boolean partialObject) throws IOException {
if (model instanceof final ClassObjectModel objectModel) {
return generateClassObjectModelTypescript(objectModel, tsGroup, imports, importWrite);
return generateClassObjectModelTypescript(objectModel, tsGroup, imports, importUpdate, importCreate,
partialObject);
}
if (model instanceof final ClassListModel listModel) {
return generateClassListModelTypescript(listModel, tsGroup, imports, importWrite);
return generateClassListModelTypescript(listModel, tsGroup, imports, importUpdate, importCreate,
partialObject);
}
if (model instanceof final ClassMapModel mapModel) {
return generateClassMapModelTypescript(mapModel, tsGroup, imports, importWrite);
return generateClassMapModelTypescript(mapModel, tsGroup, imports, importUpdate, importCreate,
partialObject);
}
if (model instanceof final ClassEnumModel enumModel) {
return generateClassEnumModelTypescript(enumModel, tsGroup, imports, importWrite);
return generateClassEnumModelTypescript(enumModel, tsGroup, imports, importUpdate, importCreate,
partialObject);
}
throw new IOException("Impossible model:" + model);
}
@ -122,7 +146,9 @@ public class TsApiGeneration {
final List<ClassModel> models,
final TsClassElementGroup tsGroup,
final Set<ClassModel> imports,
final Set<ClassModel> importWrite) throws IOException {
final Set<ClassModel> importUpdate,
final Set<ClassModel> importCreate,
final boolean partialObject) throws IOException {
if (models.size() == 0) {
return "void";
}
@ -134,7 +160,8 @@ public class TsApiGeneration {
} else {
out.append(" | ");
}
final String data = generateClassModelTypescript(model, tsGroup, imports, importWrite);
final String data = generateClassModelTypescript(model, tsGroup, imports, importUpdate, importCreate,
partialObject);
out.append(data);
}
return out.toString();
@ -159,7 +186,8 @@ public class TsApiGeneration {
final Set<ClassModel> imports = new HashSet<>();
final Set<ClassModel> zodImports = new HashSet<>();
final Set<ClassModel> isImports = new HashSet<>();
final Set<ClassModel> writeImports = new HashSet<>();
final Set<ClassModel> updateImports = new HashSet<>();
final Set<ClassModel> createImports = new HashSet<>();
final Set<String> toolImports = new HashSet<>();
for (final ApiModel interfaceElement : element.interfaces) {
final List<String> consumes = interfaceElement.consumes;
@ -172,6 +200,7 @@ public class TsApiGeneration {
data.append("\n\n");
data.append(returnComplexModel.replaceAll("(?m)^", "\t"));
for (final ClassModel elem : interfaceElement.returnTypes) {
// TODO maybe need to update this with the type of zod requested (like update, create ...
zodImports.addAll(elem.getDependencyGroupModels());
}
}
@ -195,6 +224,9 @@ public class TsApiGeneration {
if (interfaceElement.unnamedElement.size() == 1 || interfaceElement.multiPartParameters.size() != 0) {
data.append("\n\t\t\tdata,");
}
if (!interfaceElement.headers.isEmpty()) {
data.append("\n\t\t\theaders,");
}
if (needGenerateProgress) {
data.append("\n\t\t\tcallbacks,");
}
@ -207,7 +239,8 @@ public class TsApiGeneration {
data.append("\n\t\t\t");
data.append(queryEntry.getKey());
data.append("?: ");
data.append(generateClassModelsTypescript(queryEntry.getValue(), tsGroup, imports, null));
data.append(
generateClassModelsTypescript(queryEntry.getValue(), tsGroup, imports, null, null, false));
data.append(",");
}
data.append("\n\t\t},");
@ -218,15 +251,27 @@ public class TsApiGeneration {
data.append("\n\t\t\t");
data.append(paramEntry.getKey());
data.append(": ");
data.append(generateClassModelsTypescript(paramEntry.getValue(), tsGroup, imports, null));
data.append(
generateClassModelsTypescript(paramEntry.getValue(), tsGroup, imports, null, null, false));
data.append(",");
}
data.append("\n\t\t},");
}
if (interfaceElement.unnamedElement.size() == 1) {
data.append("\n\t\tdata: ");
data.append(generateClassModelTypescript(interfaceElement.unnamedElement.get(0), tsGroup, imports,
writeImports));
if (interfaceElement.restTypeRequest == RestTypeRequest.POST) {
data.append(generateClassModelTypescript(interfaceElement.unnamedElement.get(0), tsGroup, imports,
null, createImports, false));
} else if (interfaceElement.restTypeRequest == RestTypeRequest.PUT) {
data.append(generateClassModelTypescript(interfaceElement.unnamedElement.get(0), tsGroup, imports,
updateImports, null, false));
} else if (interfaceElement.restTypeRequest == RestTypeRequest.PATCH) {
data.append(generateClassModelTypescript(interfaceElement.unnamedElement.get(0), tsGroup, imports,
updateImports, null, true));
} else {
data.append(generateClassModelTypescript(interfaceElement.unnamedElement.get(0), tsGroup, imports,
null, null, true));
}
data.append(",");
} else if (interfaceElement.multiPartParameters.size() != 0) {
data.append("\n\t\tdata: {");
@ -238,8 +283,34 @@ public class TsApiGeneration {
data.append("?");
}
data.append(": ");
data.append(generateClassModelsTypescript(pathEntry.getValue().model(), tsGroup, imports,
writeImports));
if (interfaceElement.restTypeRequest == RestTypeRequest.POST) {
data.append(generateClassModelsTypescript(pathEntry.getValue().model(), tsGroup, imports, null,
createImports, false));
} else if (interfaceElement.restTypeRequest == RestTypeRequest.PUT) {
data.append(generateClassModelsTypescript(pathEntry.getValue().model(), tsGroup, imports,
updateImports, null, false));
} else if (interfaceElement.restTypeRequest == RestTypeRequest.PATCH) {
data.append(generateClassModelsTypescript(pathEntry.getValue().model(), tsGroup, imports,
updateImports, null, true));
} else {
data.append(generateClassModelsTypescript(pathEntry.getValue().model(), tsGroup, imports, null,
null, true));
}
data.append(",");
}
data.append("\n\t\t},");
}
if (!interfaceElement.headers.isEmpty()) {
data.append("\n\t\theaders?: {");
for (final Entry<String, OptionalClassModel> headerEntry : interfaceElement.headers.entrySet()) {
data.append("\n\t\t\t");
data.append(headerEntry.getKey());
if (headerEntry.getValue().optional()) {
data.append("?");
}
data.append(": ");
data.append(generateClassModelsTypescript(headerEntry.getValue().model(), tsGroup, imports, null,
null, false));
data.append(",");
}
data.append("\n\t\t},");
@ -287,7 +358,7 @@ public class TsApiGeneration {
toolImports.add("RESTRequestJson");
} else {
final String returnType = generateClassModelsTypescript(interfaceElement.returnTypes, tsGroup, imports,
null);
null, null, false);
data.append(returnType);
data.append("> {");
if ("void".equals(returnType)) {
@ -332,7 +403,7 @@ public class TsApiGeneration {
data.append("\n\t\t\t\taccept: produce,");
} else {
final String returnType = generateClassModelsTypescript(interfaceElement.returnTypes, tsGroup,
imports, null);
imports, null, null, false);
if (!"void".equals(returnType)) {
for (final String elem : produces) {
if (MediaType.APPLICATION_JSON.equals(elem)) {
@ -360,6 +431,9 @@ public class TsApiGeneration {
if (needGenerateProgress) {
data.append("\n\t\t\tcallbacks,");
}
if (!interfaceElement.headers.isEmpty()) {
data.append("\n\t\t\theaders,");
}
data.append("\n\t\t}");
if (returnComplexModel != null) {
data.append(", is");
@ -419,17 +493,30 @@ public class TsApiGeneration {
if (tsModel.nativeType == DefinedPosition.NATIVE) {
continue;
}
if (!tsModel.models.get(0).getApiGenerationMode().read()) {
continue;
}
finalImportSet.add("Zod" + tsModel.tsTypeName);
}
for (final ClassModel model : writeImports) {
for (final ClassModel model : updateImports) {
final TsClassElement tsModel = tsGroup.find(model);
if (tsModel.nativeType != DefinedPosition.NORMAL) {
continue;
}
if (tsModel.models.get(0).isNoWriteSpecificMode()) {
if (!tsModel.models.get(0).getApiGenerationMode().update()) {
continue;
}
finalImportSet.add(tsModel.tsTypeName + "Write");
finalImportSet.add(tsModel.tsTypeName + TsClassElement.MODEL_TYPE_UPDATE);
}
for (final ClassModel model : createImports) {
final TsClassElement tsModel = tsGroup.find(model);
if (tsModel.nativeType != DefinedPosition.NORMAL) {
continue;
}
if (!tsModel.models.get(0).getApiGenerationMode().create()) {
continue;
}
finalImportSet.add(tsModel.tsTypeName + TsClassElement.MODEL_TYPE_CREATE);
}
if (finalImportSet.size() != 0) {

View File

@ -27,6 +27,9 @@ public class TsClassElement {
NORMAL // Normal Object to interpret.
}
public static final String MODEL_TYPE_UPDATE = "Update";
public static final String MODEL_TYPE_CREATE = "Create";
public List<ClassModel> models;
public String zodName;
public String tsTypeName;
@ -138,7 +141,7 @@ public class TsClassElement {
out.append(this.tsTypeName);
out.append(");\n");
}
out.append(generateExportCheckFunctionWrite(""));
out.append(generateExportCheckFunctionAppended(""));
return out.toString();
}
@ -168,9 +171,9 @@ public class TsClassElement {
return out.toString();
}
private String generateExportCheckFunctionWrite(final String writeString) {
return generateExportCheckFunction(this.tsCheckType + writeString, this.tsTypeName + writeString,
this.zodName + writeString);
private String generateExportCheckFunctionAppended(final String appendString) {
return generateExportCheckFunction(this.tsCheckType + appendString, this.tsTypeName + appendString,
this.zodName + appendString);
}
public String generateImports(final List<ClassModel> depModels, final TsClassElementGroup tsGroup)
@ -180,11 +183,23 @@ public class TsClassElement {
final TsClassElement tsModel = tsGroup.find(depModel);
if (tsModel.nativeType != DefinedPosition.NATIVE) {
out.append("import {");
out.append(tsModel.zodName);
if (tsModel.nativeType == DefinedPosition.NORMAL && !(tsModel.models.get(0).isNoWriteSpecificMode())) {
if (tsModel.nativeType != DefinedPosition.NORMAL
|| tsModel.models.get(0).getApiGenerationMode().read()) {
out.append(tsModel.zodName);
}
if (tsModel.nativeType == DefinedPosition.NORMAL
&& tsModel.models.get(0).getApiGenerationMode().update()) {
out.append(", ");
out.append(tsModel.zodName);
out.append("Write ");
out.append(MODEL_TYPE_UPDATE);
out.append(" ");
}
if (tsModel.nativeType == DefinedPosition.NORMAL
&& tsModel.models.get(0).getApiGenerationMode().create()) {
out.append(", ");
out.append(tsModel.zodName);
out.append(MODEL_TYPE_CREATE);
out.append(" ");
}
out.append("} from \"./");
out.append(tsModel.fileName);
@ -321,7 +336,7 @@ public class TsClassElement {
}
public String readOnlyZod(final FieldProperty field) {
if (field.readOnly()) {
if (!field.accessLimitation().creatable() && !field.accessLimitation().updatable()) {
return ".readonly()";
}
return "";
@ -343,18 +358,32 @@ public class TsClassElement {
public String generateObject(final ClassObjectModel model, final TsClassElementGroup tsGroup) throws IOException {
final StringBuilder out = new StringBuilder();
out.append(getBaseHeader());
out.append(generateImports(model.getDependencyModels(), tsGroup));
out.append("\n");
// ------------------------------------------------------------------------
// -- Generate read mode
// ------------------------------------------------------------------------
if (model.getApiGenerationMode().read()) {
out.append(generateObjectRead(model, tsGroup));
}
if (model.getApiGenerationMode().update()) {
out.append(generateObjectUpdate(model, tsGroup));
}
if (model.getApiGenerationMode().create()) {
out.append(generateObjectCreate(model, tsGroup));
}
return out.toString();
}
public String generateObjectRead(final ClassObjectModel model, final TsClassElementGroup tsGroup)
throws IOException {
final StringBuilder out = new StringBuilder();
out.append(generateComment(model));
out.append("export const ");
out.append(this.zodName);
out.append(" = ");
// Check if the object is empty:
boolean isEmpty = model.getFields().size() == 0;
final boolean isEmpty = model.getFields().size() == 0;
if (model.getExtendsClass() != null) {
final ClassModel parentClass = model.getExtendsClass();
@ -392,98 +421,138 @@ public class TsClassElement {
out.append(optionalTypeZod(field));
out.append(",\n");
}
final List<String> omitField = model.getReadOnlyField();
if (model.getExtendsClass() != null && isEmpty) {
out.append(";\n");
} else {
out.append("\n});\n");
}
out.append(generateZodInfer(this.tsTypeName, this.zodName));
out.append(generateExportCheckFunctionWrite(""));
// check if we need to generate write mode :
if (!model.isNoWriteSpecificMode()) {
// ------------------------------------------------------------------------
// -- Generate write mode
// ------------------------------------------------------------------------
//out.append(generateComment(model));
out.append("export const ");
out.append(this.zodName);
out.append("Write = ");
isEmpty = model.getFields().stream().filter(field -> !field.readOnly()).count() == 0;
if (model.getExtendsClass() != null) {
final ClassModel parentClass = model.getExtendsClass();
final TsClassElement tsParentModel = tsGroup.find(parentClass);
out.append(tsParentModel.zodName);
out.append("Write");
if (!isEmpty) {
out.append(".extend({\n");
}
} else {
out.append("zod.object({\n");
out.append(generateExportCheckFunctionAppended(""));
return out.toString();
}
public String generateObjectUpdate(final ClassObjectModel model, final TsClassElementGroup tsGroup)
throws IOException {
final StringBuilder out = new StringBuilder();
final String modeleType = MODEL_TYPE_UPDATE;
out.append("export const ");
out.append(this.zodName);
out.append(modeleType);
out.append(" = ");
// Check if at minimum One fiend is updatable to generate the local object
final boolean isEmpty = model.getFields().stream().filter(field -> field.accessLimitation().updatable())
.count() == 0;
if (model.getExtendsClass() != null) {
final ClassModel parentClass = model.getExtendsClass();
final TsClassElement tsParentModel = tsGroup.find(parentClass);
out.append(tsParentModel.zodName);
out.append(modeleType);
if (!isEmpty) {
out.append(".extend({\n");
}
for (final FieldProperty field : model.getFields()) {
// remove all readOnly field
if (field.readOnly()) {
continue;
}
final ClassModel fieldModel = field.model();
if (field.comment() != null) {
out.append("\t/**\n");
out.append("\t * ");
out.append(field.comment());
out.append("\n\t */\n");
}
out.append("\t");
out.append(field.name());
out.append(": ");
if (fieldModel instanceof ClassEnumModel || fieldModel instanceof ClassObjectModel) {
final TsClassElement tsFieldModel = tsGroup.find(fieldModel);
out.append(tsFieldModel.zodName);
} else if (fieldModel instanceof final ClassListModel fieldListModel) {
final String data = generateTsList(fieldListModel, tsGroup);
out.append(data);
} else if (fieldModel instanceof final ClassMapModel fieldMapModel) {
final String data = generateTsMap(fieldMapModel, tsGroup);
out.append(data);
}
out.append(maxSizeZod(field));
out.append(optionalWriteTypeZod(field));
// all write field are optional
if (field.model() instanceof final ClassObjectModel plop) {
if (!plop.isPrimitive()) {
out.append(".optional()");
}
} else {
out.append(".optional()");
}
out.append(",\n");
}
if (model.getExtendsClass() != null && isEmpty) {
out.append(";\n");
} else {
out.append("\n});\n");
}
out.append(generateZodInfer(this.tsTypeName + "Write", this.zodName + "Write"));
// Check only the input value ==> no need of the output
out.append(generateExportCheckFunctionWrite("Write"));
// Generate the Write Type associated.
/*
out.append("\nexport const ");
out.append(this.zodName);
out.append("Write = ");
out.append(this.zodName);
if (omitField.size() != 0) {
out.append(".omit({\n");
for (final String elem : omitField) {
out.append("\t");
out.append(elem);
out.append(": true,\n");
}
out.append("\n})");
}
out.append(".partial();\n");
*/
} else {
out.append("zod.object({\n");
}
for (final FieldProperty field : model.getFields()) {
// remove all readOnly field
if (!field.accessLimitation().updatable()) {
continue;
}
final ClassModel fieldModel = field.model();
if (field.comment() != null) {
out.append("\t/**\n");
out.append("\t * ");
out.append(field.comment());
out.append("\n\t */\n");
}
out.append("\t");
out.append(field.name());
out.append(": ");
if (fieldModel instanceof ClassEnumModel || fieldModel instanceof ClassObjectModel) {
final TsClassElement tsFieldModel = tsGroup.find(fieldModel);
out.append(tsFieldModel.zodName);
} else if (fieldModel instanceof final ClassListModel fieldListModel) {
final String data = generateTsList(fieldListModel, tsGroup);
out.append(data);
} else if (fieldModel instanceof final ClassMapModel fieldMapModel) {
final String data = generateTsMap(fieldMapModel, tsGroup);
out.append(data);
}
out.append(maxSizeZod(field));
out.append(optionalWriteTypeZod(field));
out.append(optionalTypeZod(field));
out.append(",\n");
}
if (model.getExtendsClass() != null && isEmpty) {
out.append(";\n");
} else {
out.append("\n});\n");
}
out.append(generateZodInfer(this.tsTypeName + modeleType, this.zodName + modeleType));
// Check only the input value ==> no need of the output
out.append(generateExportCheckFunctionAppended(modeleType));
return out.toString();
}
public String generateObjectCreate(final ClassObjectModel model, final TsClassElementGroup tsGroup)
throws IOException {
final StringBuilder out = new StringBuilder();
final String modeleType = MODEL_TYPE_CREATE;
out.append("export const ");
out.append(this.zodName);
out.append(modeleType);
out.append(" = ");
final boolean isEmpty = model.getFields().stream().filter(field -> field.accessLimitation().creatable())
.count() == 0;
if (model.getExtendsClass() != null) {
final ClassModel parentClass = model.getExtendsClass();
final TsClassElement tsParentModel = tsGroup.find(parentClass);
out.append(tsParentModel.zodName);
out.append(modeleType);
if (!isEmpty) {
out.append(".extend({\n");
}
} else {
out.append("zod.object({\n");
}
for (final FieldProperty field : model.getFields()) {
// remove all readOnly field
if (!field.accessLimitation().creatable()) {
continue;
}
final ClassModel fieldModel = field.model();
if (field.comment() != null) {
out.append("\t/**\n");
out.append("\t * ");
out.append(field.comment());
out.append("\n\t */\n");
}
out.append("\t");
out.append(field.name());
out.append(": ");
if (fieldModel instanceof ClassEnumModel || fieldModel instanceof ClassObjectModel) {
final TsClassElement tsFieldModel = tsGroup.find(fieldModel);
out.append(tsFieldModel.zodName);
} else if (fieldModel instanceof final ClassListModel fieldListModel) {
final String data = generateTsList(fieldListModel, tsGroup);
out.append(data);
} else if (fieldModel instanceof final ClassMapModel fieldMapModel) {
final String data = generateTsMap(fieldMapModel, tsGroup);
out.append(data);
}
out.append(maxSizeZod(field));
out.append(optionalWriteTypeZod(field));
out.append(optionalTypeZod(field));
out.append(",\n");
}
if (model.getExtendsClass() != null && isEmpty) {
out.append(";\n");
} else {
out.append("\n});\n");
}
out.append(generateZodInfer(this.tsTypeName + modeleType, this.zodName + modeleType));
// Check only the input value ==> no need of the output
out.append(generateExportCheckFunctionAppended(modeleType));
return out.toString();
}

View File

@ -1,6 +1,7 @@
package org.kar.archidata.model;
import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import com.fasterxml.jackson.annotation.JsonInclude;
@ -16,12 +17,15 @@ public class Data extends OIDGenericDataSoftDelete {
@Column(length = 128, nullable = false)
@Schema(description = "Sha512 of the data")
@Size(max = 512)
@ApiAccessLimitation(creatable = false, updatable = false)
public String sha512;
@Column(length = 128, nullable = false)
@Schema(description = "Mime -type of the media")
@Size(max = 512)
@ApiAccessLimitation(creatable = false, updatable = false)
public String mimeType;
@Column(nullable = false)
@Schema(description = "Size in Byte of the data")
@ApiAccessLimitation(creatable = false, updatable = false)
public Long size;
}

View File

@ -1,15 +1,20 @@
package org.kar.archidata.model;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@ApiGenerationMode(create = true, update = true)
public class GenericData extends GenericTiming {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, unique = true)
@Schema(description = "Unique Id of the object", required = false, readOnly = true, example = "123456")
@Schema(description = "Unique Id of the object", example = "123456")
@ApiAccessLimitation(creatable = false, updatable = false)
public Long id = null;
}

View File

@ -2,18 +2,22 @@ package org.kar.archidata.model;
import org.kar.archidata.annotation.DataDeleted;
import org.kar.archidata.annotation.DataNotRead;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.ws.rs.DefaultValue;
@ApiGenerationMode(create = true, update = true)
public class GenericDataSoftDelete extends GenericData {
@DataNotRead
@Column(nullable = false)
@DefaultValue("'0'")
@DataDeleted
@Schema(description = "Deleted state", hidden = true, required = false, readOnly = true)
@Schema(description = "Deleted state", hidden = true)
@Nullable
@ApiAccessLimitation(creatable = false, updatable = false)
public Boolean deleted = null;
}

View File

@ -5,6 +5,8 @@ import java.util.Date;
import org.kar.archidata.annotation.CreationTimestamp;
import org.kar.archidata.annotation.DataNotRead;
import org.kar.archidata.annotation.UpdateTimestamp;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import com.fasterxml.jackson.annotation.JsonFormat;
@ -12,20 +14,22 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
@ApiGenerationMode(create = true, update = true)
public class GenericTiming {
@DataNotRead
@CreationTimestamp
@Column(nullable = false, insertable = false, updatable = false)
@Schema(description = "Create time of the object", required = false, example = "2000-01-23T01:23:45.678+01:00", readOnly = true)
@Schema(description = "Create time of the object", example = "2000-01-23T01:23:45.678+01:00")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
@Nullable
@ApiAccessLimitation(creatable = false, updatable = false)
public Date createdAt = null;
@DataNotRead
@UpdateTimestamp
@Column(nullable = false, insertable = false, updatable = false)
@Schema(description = "When update the object", required = false, example = "2000-01-23T00:23:45.678Z", readOnly = true)
@Schema(description = "When update the object", example = "2000-01-23T00:23:45.678Z")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
// public Instant updatedAt = null;
@Nullable
@ApiAccessLimitation(creatable = false, updatable = false)
public Date updatedAt = null;
}

View File

@ -1,17 +1,21 @@
package org.kar.archidata.model;
import org.bson.types.ObjectId;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import dev.morphia.annotations.Id;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.validation.constraints.NotNull;
@ApiGenerationMode(create = true, update = true)
public class OIDGenericData extends GenericTiming {
@Id
@jakarta.persistence.Id
@Column(nullable = false, unique = true, name = "_id")
@Schema(description = "Unique ObjectID of the object", required = false, readOnly = true, example = "65161616841351")
@Schema(description = "Unique ObjectID of the object", example = "65161616841351")
@NotNull
@ApiAccessLimitation(creatable = false, updatable = false)
public ObjectId oid = null;
}

View File

@ -2,18 +2,22 @@ package org.kar.archidata.model;
import org.kar.archidata.annotation.DataDeleted;
import org.kar.archidata.annotation.DataNotRead;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.ws.rs.DefaultValue;
@ApiGenerationMode(create = true, update = true)
public class OIDGenericDataSoftDelete extends OIDGenericData {
@DataNotRead
@Column(nullable = false)
@DefaultValue("'0'")
@DataDeleted
@Schema(description = "Deleted state", hidden = true, required = false, readOnly = true)
@Schema(description = "Deleted state", hidden = true)
@Nullable
@ApiAccessLimitation(creatable = false, updatable = false)
public Boolean deleted = null;
}

View File

@ -2,17 +2,22 @@ package org.kar.archidata.model;
import java.util.UUID;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.DefaultValue;
@ApiGenerationMode(create = true, update = true)
public class UUIDGenericData extends GenericTiming {
@Id
@DefaultValue("(UUID_TO_BIN(UUID(), TRUE))")
@Column(nullable = false, unique = true)
@Schema(description = "Unique UUID of the object", required = false, readOnly = true, example = "e6b33c1c-d24d-11ee-b616-02420a030102")
@Schema(description = "Unique UUID of the object", example = "e6b33c1c-d24d-11ee-b616-02420a030102")
@NotNull
@ApiAccessLimitation(creatable = false, updatable = false)
public UUID uuid = null;
}

View File

@ -2,18 +2,22 @@ package org.kar.archidata.model;
import org.kar.archidata.annotation.DataDeleted;
import org.kar.archidata.annotation.DataNotRead;
import org.kar.archidata.annotation.apiGenerator.ApiAccessLimitation;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.ws.rs.DefaultValue;
@ApiGenerationMode(create = true, update = true)
public class UUIDGenericDataSoftDelete extends UUIDGenericData {
@DataNotRead
@Column(nullable = false)
@DefaultValue("'0'")
@DataDeleted
@Schema(description = "Deleted state", hidden = true, required = false, readOnly = true)
@Schema(description = "Deleted state", hidden = true)
@Nullable
@ApiAccessLimitation(creatable = false, updatable = false)
public Boolean deleted = null;
}

View File

@ -0,0 +1,48 @@
package org.kar.archidata.tools;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
public class AnnotationCreator {
@SuppressWarnings("unchecked")
public static <A extends Annotation> A createAnnotation(final Class<A> annotationClass, final Object... values) {
return (A) Proxy.newProxyInstance(annotationClass.getClassLoader(), new Class<?>[] { annotationClass },
new InvocationHandler() {
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
if ("annotationType".equals(method.getName())) {
return annotationClass;
}
if ("toString".equals(method.getName())) {
return "@" + annotationClass.getName() + values;
}
for (int i = 0; i < values.length; i += 2) {
if (method.getName().equals(values[i])) {
return values[i + 1];
}
}
return method.getDefaultValue();
}
});
}
public static void main(final String[] args) {
final ApiGenerationMode myAnnotation = AnnotationCreator.createAnnotation(ApiGenerationMode.class, "readable",
true, "creatable", false, "updatable", false);
System.out.println("readable: " + myAnnotation.read()); // Output: example
System.out.println("creatable: " + myAnnotation.create()); // Output: 100
System.out.println("updatable: " + myAnnotation.update()); // Output: 100
final ApiGenerationMode myAnnotation2 = AnnotationCreator.createAnnotation(ApiGenerationMode.class);
System.out.println("readable: " + myAnnotation2.read()); // Output: example
System.out.println("creatable: " + myAnnotation2.create()); // Output: 100
System.out.println("updatable: " + myAnnotation2.update()); // Output: 100
}
}

View File

@ -3,30 +3,29 @@
* @copyright 2024, Edouard DUPIN, all right reserved
* @license MPL-2
*/
import { RestErrorResponse, isRestErrorResponse } from "./model";
import { RestErrorResponse, isRestErrorResponse } from './model';
export enum HTTPRequestModel {
ARCHIVE = "ARCHIVE",
DELETE = "DELETE",
HEAD = "HEAD",
GET = "GET",
OPTION = "OPTION",
PATCH = "PATCH",
POST = "POST",
PUT = "PUT",
RESTORE = "RESTORE",
ARCHIVE = 'ARCHIVE',
DELETE = 'DELETE',
HEAD = 'HEAD',
GET = 'GET',
OPTION = 'OPTION',
PATCH = 'PATCH',
POST = 'POST',
PUT = 'PUT',
RESTORE = 'RESTORE',
}
export enum HTTPMimeType {
ALL = "*/*",
CSV = "text/csv",
IMAGE = "image/*",
IMAGE_JPEG = "image/jpeg",
IMAGE_PNG = "image/png",
JSON = "application/json",
MULTIPART = "multipart/form-data",
OCTET_STREAM = "application/octet-stream",
TEXT_PLAIN = "text/plain",
ALL = '*/*',
CSV = 'text/csv',
IMAGE = 'image/*',
IMAGE_JPEG = 'image/jpeg',
IMAGE_PNG = 'image/png',
JSON = 'application/json',
MULTIPART = 'multipart/form-data',
OCTET_STREAM = 'application/octet-stream',
TEXT_PLAIN = 'text/plain',
}
export interface RESTConfig {
@ -56,12 +55,11 @@ export interface ModelResponseHttp {
export type ErrorRestApiCallback = (response: Response) => void;
let errorApiGlobalCallback: ErrorRestApiCallback|undefined = undefined;
export const setErrorApiGlobalCallback = (callback:ErrorRestApiCallback) => {
errorApiGlobalCallback = callback;
}
let errorApiGlobalCallback: ErrorRestApiCallback | undefined = undefined;
export const setErrorApiGlobalCallback = (callback: ErrorRestApiCallback) => {
errorApiGlobalCallback = callback;
};
function isNullOrUndefined(data: any): data is undefined | null {
return data === undefined || data === null;
@ -87,6 +85,7 @@ export interface RESTRequestType {
data?: any;
params?: object;
queries?: object;
headers?: any;
callbacks?: RESTCallbacks;
}
@ -96,15 +95,15 @@ function replaceAll(input, searchValue, replaceValue) {
function removeTrailingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "undefined";
return 'undefined';
}
return input.replace(/\/+$/, "");
return input.replace(/\/+$/, '');
}
function removeLeadingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "";
return '';
}
return input.replace(/^\/+/, "");
return input.replace(/^\/+/, '');
}
export function RESTUrl({
@ -142,9 +141,9 @@ export function RESTUrl({
}
}
if (restConfig.token !== undefined && restModel.tokenInUrl === true) {
searchParams.append("Authorization", `Bearer ${restConfig.token}`);
searchParams.append('Authorization', `Bearer ${restConfig.token}`);
}
return generateUrl + "?" + searchParams.toString();
return generateUrl + '?' + searchParams.toString();
}
export function fetchProgress(
@ -168,7 +167,7 @@ export function fetchProgress(
return new Promise((resolve, reject) => {
// Stream the upload progress
if (progressUpload) {
xhr.io?.upload.addEventListener("progress", (dataEvent) => {
xhr.io?.upload.addEventListener('progress', (dataEvent) => {
if (dataEvent.lengthComputable) {
progressUpload(dataEvent.loaded, dataEvent.total);
}
@ -176,7 +175,7 @@ export function fetchProgress(
}
// Stream the download progress
if (progressDownload) {
xhr.io?.addEventListener("progress", (dataEvent) => {
xhr.io?.addEventListener('progress', (dataEvent) => {
if (dataEvent.lengthComputable) {
progressDownload(dataEvent.loaded, dataEvent.total);
}
@ -196,19 +195,19 @@ export function fetchProgress(
};
}
// Check if we have an internal Fail:
xhr.io?.addEventListener("error", () => {
xhr.io?.addEventListener('error', () => {
xhr.io = undefined;
reject(new TypeError("Failed to fetch"));
reject(new TypeError('Failed to fetch'));
});
// Capture the end of the stream
xhr.io?.addEventListener("loadend", () => {
xhr.io?.addEventListener('loadend', () => {
if (xhr.io?.readyState !== XMLHttpRequest.DONE) {
return;
}
if (xhr.io?.status === 0) {
//the stream has been aborted
reject(new TypeError("Fetch has been aborted"));
reject(new TypeError('Fetch has been aborted'));
return;
}
// Stream is ended, transform in a generic response:
@ -218,17 +217,17 @@ export function fetchProgress(
});
const headersArray = replaceAll(
xhr.io.getAllResponseHeaders().trim(),
"\r\n",
"\n"
).split("\n");
'\r\n',
'\n'
).split('\n');
headersArray.forEach(function (header) {
const firstColonIndex = header.indexOf(":");
const firstColonIndex = header.indexOf(':');
if (firstColonIndex !== -1) {
const key = header.substring(0, firstColonIndex).trim();
const value = header.substring(firstColonIndex + 1).trim();
response.headers.set(key, value);
} else {
response.headers.set(header, "");
response.headers.set(header, '');
}
});
xhr.io = undefined;
@ -250,27 +249,29 @@ export function RESTRequest({
data,
params,
queries,
headers = {},
callbacks,
}: RESTRequestType): Promise<ModelResponseHttp> {
// Create the URL PATH:
let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries });
let headers: any = {};
if (restConfig.token !== undefined && restModel.tokenInUrl !== true) {
headers["Authorization"] = `Bearer ${restConfig.token}`;
headers['Authorization'] = `Bearer ${restConfig.token}`;
}
if (restModel.accept !== undefined) {
headers["Accept"] = restModel.accept;
headers['Accept'] = restModel.accept;
}
if (restModel.requestType !== HTTPRequestModel.GET &&
restModel.requestType !== HTTPRequestModel.ARCHIVE &&
restModel.requestType !== HTTPRequestModel.RESTORE
if (
restModel.requestType !== HTTPRequestModel.GET &&
restModel.requestType !== HTTPRequestModel.ARCHIVE &&
restModel.requestType !== HTTPRequestModel.RESTORE
) {
// if Get we have not a content type, the body is empty
if (restModel.contentType !== HTTPMimeType.MULTIPART &&
restModel.contentType !== undefined
) {
if (
restModel.contentType !== HTTPMimeType.MULTIPART &&
restModel.contentType !== undefined
) {
// special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****"
headers["Content-Type"] = restModel.contentType;
headers['Content-Type'] = restModel.contentType;
}
}
let body = data;
@ -311,23 +312,27 @@ export function RESTRequest({
}
action
.then((response: Response) => {
if(errorApiGlobalCallback && 400 <= response.status && response.status <= 499) {
// Detect an error and trigger the generic error callback:
errorApiGlobalCallback(response);
}
if (
errorApiGlobalCallback &&
400 <= response.status &&
response.status <= 499
) {
// Detect an error and trigger the generic error callback:
errorApiGlobalCallback(response);
}
if (response.status >= 200 && response.status <= 299) {
const contentType = response.headers.get("Content-Type");
const contentType = response.headers.get('Content-Type');
if (
!isNullOrUndefined(restModel.accept) &&
restModel.accept !== contentType
) {
reject({
name: "Model accept type incompatible",
name: 'Model accept type incompatible',
time: Date().toString(),
status: 901,
message: `REST Content type are not compatible: ${restModel.accept} != ${contentType}`,
statusMessage: "Fetch error",
error: "rest-tools.ts Wrong type in the message return type",
statusMessage: 'Fetch error',
error: 'rest-tools.ts Wrong type in the message return type',
} as RestErrorResponse);
} else if (contentType === HTTPMimeType.JSON) {
response
@ -337,12 +342,12 @@ export function RESTRequest({
})
.catch((reason: Error) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: 902,
message: `REST parse json fail: ${reason}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
});
} else {
@ -362,22 +367,22 @@ export function RESTRequest({
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: 903,
message: `REST parse error json with wrong type fail. ${dataError}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch ERROR parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
});
}
@ -387,22 +392,22 @@ export function RESTRequest({
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ${dataError} with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch ERROR TEXT parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT FAIL",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch ERROR TEXT FAIL',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
});
});
@ -413,12 +418,12 @@ export function RESTRequest({
reject(error);
} else {
reject({
name: "Request fail",
name: 'Request fail',
time: Date(),
status: 999,
message: error,
statusMessage: "Fetch catch error",
error: "rest-tools.ts detect an error in the fetch request",
statusMessage: 'Fetch catch error',
error: 'rest-tools.ts detect an error in the fetch request',
});
}
});
@ -439,12 +444,12 @@ export function RESTRequestJson<TYPE>(
resolve(value.data);
} else {
reject({
name: "Model check fail",
name: 'Model check fail',
time: Date().toString(),
status: 950,
error: "REST Fail to verify the data",
statusMessage: "API cast ERROR",
message: "api.ts Check type as fail",
error: 'REST Fail to verify the data',
statusMessage: 'API cast ERROR',
message: 'api.ts Check type as fail',
} as RestErrorResponse);
}
})

View File

@ -4,7 +4,7 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.apiGenerator.ApiAsyncType;
import org.kar.archidata.annotation.method.ARCHIVE;
import org.kar.archidata.annotation.method.RESTORE;
import org.kar.archidata.exception.NotFoundException;
@ -100,7 +100,7 @@ public class TestResource {
@Consumes(MediaType.APPLICATION_JSON)
public SimpleArchiveTable patch(
@PathParam("id") final Long id,
@AsyncType(SimpleArchiveTable.class) final String jsonRequest) throws Exception {
@ApiAsyncType(SimpleArchiveTable.class) final String jsonRequest) throws Exception {
LOGGER.info("patch({})", id);
throw new NotFoundException("element does not exist: " + id);
}

View File

@ -2,7 +2,7 @@ package test.kar.archidata.apiExtern.resource;
import java.util.List;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.apiGenerator.ApiAsyncType;
import org.kar.archidata.annotation.method.ARCHIVE;
import org.kar.archidata.annotation.method.RESTORE;
import org.kar.archidata.dataAccess.DataAccess;
@ -66,7 +66,7 @@ public class TestResourceSample {
@Path("{id}")
@PermitAll
@Consumes(MediaType.APPLICATION_JSON)
public SimpleTable patch(@PathParam("id") final Long id, @AsyncType(SimpleTable.class) final String jsonRequest)
public SimpleTable patch(@PathParam("id") final Long id, @ApiAsyncType(SimpleTable.class) final String jsonRequest)
throws Exception {
DataAccess.updateWithJson(SimpleTable.class, id, jsonRequest);
return DataAccess.get(SimpleTable.class, id);