Skip to content

Commit 6fad27e

Browse files
committed
feat(annotation): add @SbxModel and SbxEntity for type-safe model operations
- Add @SbxModel annotation for mapping classes to SBX model names - Add SbxEntity interface for standard key/meta fields in records - Add SbxModels utility for extracting model names from annotations - Add FindQuery.from(Class<?>) to infer model from annotation - Add type-safe SBXService methods: find(Class), create(entity), update(entity), delete(entity), delete(Class, keys) - Update InventoryHistory test model to use annotations
1 parent 16b9eb0 commit 6fad27e

8 files changed

Lines changed: 368 additions & 2 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.sbxcloud.sbx.annotation;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.sbxcloud.sbx.model.SBXMeta;
5+
6+
/**
7+
* Interface for SBX entities with standard key and meta fields.
8+
* <p>
9+
* Records implementing this interface automatically get the standard SBX fields.
10+
* Use with @SbxModel for full annotation support.
11+
*
12+
* <pre>{@code
13+
* @SbxModel("inventory_history")
14+
* @JsonIgnoreProperties(ignoreUnknown = true)
15+
* public record InventoryHistory(
16+
* @JsonProperty("_KEY") String key,
17+
* @JsonProperty("_META") SBXMeta meta,
18+
* String masterlist,
19+
* Integer week
20+
* ) implements SbxEntity {}
21+
* }</pre>
22+
*/
23+
public interface SbxEntity {
24+
25+
/**
26+
* Returns the SBX primary key (_KEY).
27+
*/
28+
@JsonProperty("_KEY")
29+
String key();
30+
31+
/**
32+
* Returns the SBX metadata (_META).
33+
*/
34+
@JsonProperty("_META")
35+
SBXMeta meta();
36+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.sbxcloud.sbx.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* Marks a class as an SBX model and specifies its row_model name.
10+
*
11+
* <pre>{@code
12+
* @SbxModel("inventory_history")
13+
* public record InventoryHistory(
14+
* @JsonProperty("_KEY") String key,
15+
* @JsonProperty("_META") SBXMeta meta,
16+
* String masterlist,
17+
* Integer week,
18+
* Double price,
19+
* Integer quantity
20+
* ) {}
21+
* }</pre>
22+
*/
23+
@Target(ElementType.TYPE)
24+
@Retention(RetentionPolicy.RUNTIME)
25+
public @interface SbxModel {
26+
/**
27+
* The SBX row_model name.
28+
*/
29+
String value();
30+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.sbxcloud.sbx.annotation;
2+
3+
/**
4+
* Utility for working with @SbxModel annotations.
5+
*/
6+
public final class SbxModels {
7+
8+
private SbxModels() {}
9+
10+
/**
11+
* Extracts the model name from a class annotated with @SbxModel.
12+
*
13+
* @param type the annotated class
14+
* @return the model name
15+
* @throws IllegalArgumentException if class is not annotated with @SbxModel
16+
*/
17+
public static String getModelName(Class<?> type) {
18+
var annotation = type.getAnnotation(SbxModel.class);
19+
if (annotation == null) {
20+
throw new IllegalArgumentException(
21+
"Class " + type.getName() + " is not annotated with @SbxModel"
22+
);
23+
}
24+
return annotation.value();
25+
}
26+
27+
/**
28+
* Checks if a class is annotated with @SbxModel.
29+
*/
30+
public static boolean isAnnotated(Class<?> type) {
31+
return type.isAnnotationPresent(SbxModel.class);
32+
}
33+
}

src/main/java/com/sbxcloud/sbx/client/SBXService.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.fasterxml.jackson.databind.ObjectMapper;
66
import com.fasterxml.jackson.databind.SerializationFeature;
77
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
8+
import com.sbxcloud.sbx.annotation.SbxEntity;
9+
import com.sbxcloud.sbx.annotation.SbxModels;
810
import com.sbxcloud.sbx.exception.SBXException;
911
import com.sbxcloud.sbx.model.*;
1012
import com.sbxcloud.sbx.query.FindQuery;
@@ -201,6 +203,119 @@ public SBXResponse<Void> delete(String model, String key) {
201203
return delete(model, List.of(key));
202204
}
203205

206+
// ==================== Type-safe Operations (with @SbxModel) ====================
207+
208+
/**
209+
* Finds all records for an @SbxModel annotated class.
210+
*
211+
* @param type class annotated with @SbxModel
212+
* @return find response with results
213+
*/
214+
public <T> SBXFindResponse<T> find(Class<T> type) {
215+
return find(FindQuery.from(type), type);
216+
}
217+
218+
/**
219+
* Creates a new record from an @SbxModel annotated entity.
220+
* Model name is inferred from the @SbxModel annotation.
221+
*
222+
* @param entity the entity to create
223+
* @return response with created key
224+
*/
225+
public <T> SBXResponse<T> create(T entity) {
226+
String model = SbxModels.getModelName(entity.getClass());
227+
Map<String, Object> row = objectMapper.convertValue(entity, new TypeReference<>() {});
228+
return create(model, row);
229+
}
230+
231+
/**
232+
* Creates multiple records from @SbxModel annotated entities.
233+
* Model name is inferred from the @SbxModel annotation of the first entity.
234+
*
235+
* @param entities the entities to create
236+
* @return response with created keys
237+
*/
238+
@SafeVarargs
239+
public final <T> SBXResponse<T> create(T... entities) {
240+
if (entities == null || entities.length == 0) {
241+
return SBXResponse.failure("No entities provided");
242+
}
243+
String model = SbxModels.getModelName(entities[0].getClass());
244+
List<Map<String, Object>> rows = Arrays.stream(entities)
245+
.map(e -> objectMapper.convertValue(e, new TypeReference<Map<String, Object>>() {}))
246+
.toList();
247+
return create(model, rows);
248+
}
249+
250+
/**
251+
* Updates an @SbxModel annotated entity.
252+
* The entity must have a non-null key.
253+
*
254+
* @param entity the entity to update
255+
* @return response
256+
*/
257+
public <T> SBXResponse<T> update(T entity) {
258+
String model = SbxModels.getModelName(entity.getClass());
259+
Map<String, Object> row = objectMapper.convertValue(entity, new TypeReference<>() {});
260+
return update(model, row);
261+
}
262+
263+
/**
264+
* Updates multiple @SbxModel annotated entities.
265+
*
266+
* @param entities the entities to update
267+
* @return response
268+
*/
269+
@SafeVarargs
270+
public final <T> SBXResponse<T> update(T... entities) {
271+
if (entities == null || entities.length == 0) {
272+
return SBXResponse.failure("No entities provided");
273+
}
274+
String model = SbxModels.getModelName(entities[0].getClass());
275+
List<Map<String, Object>> rows = Arrays.stream(entities)
276+
.map(e -> objectMapper.convertValue(e, new TypeReference<Map<String, Object>>() {}))
277+
.toList();
278+
return update(model, rows);
279+
}
280+
281+
/**
282+
* Deletes an @SbxEntity by extracting its key.
283+
*
284+
* @param entity the entity to delete (must implement SbxEntity)
285+
* @return response
286+
*/
287+
public <T extends SbxEntity> SBXResponse<Void> delete(T entity) {
288+
if (entity.key() == null) {
289+
return SBXResponse.failure("Entity has no key");
290+
}
291+
String model = SbxModels.getModelName(entity.getClass());
292+
return delete(model, entity.key());
293+
}
294+
295+
/**
296+
* Deletes records by keys using @SbxModel annotation for model name.
297+
*
298+
* @param type class annotated with @SbxModel
299+
* @param keys the keys to delete
300+
* @return response
301+
*/
302+
public SBXResponse<Void> delete(Class<?> type, String... keys) {
303+
String model = SbxModels.getModelName(type);
304+
return delete(model, List.of(keys));
305+
}
306+
307+
/**
308+
* Deletes records by keys using @SbxModel annotation for model name.
309+
*
310+
* @param type class annotated with @SbxModel
311+
* @param keys the keys to delete
312+
* @return response
313+
*/
314+
public SBXResponse<Void> delete(Class<?> type, List<String> keys) {
315+
String model = SbxModels.getModelName(type);
316+
return delete(model, keys);
317+
}
318+
204319
// ==================== Authentication ====================
205320

206321
/**

src/main/java/com/sbxcloud/sbx/query/FindQuery.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.sbxcloud.sbx.query;
22

3+
import com.sbxcloud.sbx.annotation.SbxModels;
34
import com.sbxcloud.sbx.model.*;
45

56
import java.util.ArrayList;
@@ -40,12 +41,22 @@ private FindQuery(String model) {
4041
}
4142

4243
/**
43-
* Creates a new query for the given model.
44+
* Creates a new query for the given model name.
4445
*/
4546
public static FindQuery from(String model) {
4647
return new FindQuery(model);
4748
}
4849

50+
/**
51+
* Creates a new query for a class annotated with @SbxModel.
52+
*
53+
* @param type class annotated with @SbxModel
54+
* @throws IllegalArgumentException if class is not annotated
55+
*/
56+
public static FindQuery from(Class<?> type) {
57+
return new FindQuery(SbxModels.getModelName(type));
58+
}
59+
4960
// ==================== Group Operations ====================
5061

5162
/**

src/test/java/com/sbxcloud/sbx/FindQueryTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.sbxcloud.sbx.model.AndOr;
55
import com.sbxcloud.sbx.model.Operation;
66
import com.sbxcloud.sbx.model.WhereClause;
7+
import com.sbxcloud.sbx.model.InventoryHistory;
78
import com.sbxcloud.sbx.query.FindQuery;
89
import org.junit.jupiter.api.Test;
910

@@ -155,4 +156,25 @@ void shouldEscapePercentInContains() {
155156
var expr = conditions.groups().get(0).group().get(0);
156157
assertEquals("%100%", expr.value());
157158
}
159+
160+
@Test
161+
void shouldBuildQueryFromAnnotatedClass() {
162+
// InventoryHistory is annotated with @SbxModel("inventory_history")
163+
var query = FindQuery.from(InventoryHistory.class)
164+
.andWhereIsGreaterThan("price", 10)
165+
.setPageSize(25)
166+
.compile();
167+
168+
assertEquals("inventory_history", query.rowModel());
169+
assertNotNull(query.where());
170+
assertEquals(25, query.size());
171+
}
172+
173+
@Test
174+
void shouldThrowForNonAnnotatedClass() {
175+
// String is not annotated with @SbxModel
176+
assertThrows(IllegalArgumentException.class, () -> {
177+
FindQuery.from(String.class);
178+
});
179+
}
158180
}

0 commit comments

Comments
 (0)