Skip to content

Commit a364d84

Browse files
delchevclaude
andauthored
feat(numbering): sdk.numbering.DocumentNumbers — client SDK (N3a) (#6385)
* feat(numbering): per-tenant document-number counter store + runtime + shell settings (N2) N2 of first-class document numbering (kf-catalog UPSTREAM_PLAN #11a): the runtime the DSL (N1, #6382) will generate against, plus the application-shell settings surface. New module components/engine/engine-numbering: - DocumentNumberStore — a per-tenant DIRIGIBLE_DOCUMENT_NUMBERS table (via the tenant-routed default datasource + SqlFactory, like DIRIGIBLE_CONFIGURATIONS), keyed by (series, scope). allocate() is gap-free: the increment takes a row lock so concurrent allocations of the same counter serialize; the row is created on first use. - DocumentNumberService — allocate + the format grammar ({seq}/{seq:0N} zero-pad, {series}, and scope tokens {year}/{<Field>}) + scope-key derivation; plus list()/setNext() for management. - DocumentNumberEndpoint — /services/core/numbering (ADMINISTRATOR/OPERATOR): GET the tenant's counters, PUT the next value a (series, scope) counter will allocate. Application shell (resources-application) — a built-in "Document Numbering" Settings entry (next to Region & Language / Tenant Configuration): lists the counters and lets an admin set each series' next value (reset/seed a sequence, e.g. start invoices at 1000); read-only on a 403. Verified on a booted instance: GET [] then PUT {series:SalesInvoice,next:1000} (204) then GET shows counter 999 (next allocate = 1000); the settings page + appShell.js serve the new entry. DocumentNumberServiceTest covers the format renderer + scope-key. The SDK entry + generated stamping + idempotency are N3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(numbering): sdk.numbering.DocumentNumbers — client SDK for first-class numbering (N3a) The SDK bridge from client-Java to the per-tenant counter runtime (N2, #6384): allocate the next gap-free number for a series and render it through the format. Both hand-written custom/ code and the forthcoming generated stamping (N3b) share one engine + one sequence + the same counter the application shell's Document Numbering settings manage. - DocumentNumbers.next(series, format, scope) / next(series, format) → the formatted number, via Beans.get(DocumentNumberService). - api-modules-java depends on engine-numbering (same precedent as engine-document for sdk.print). Verified: NumberingSdkIT drops a client-Java @controller calling DocumentNumbers.next and asserts a gap-free formatted sequence over HTTP (T-0001, T-0002) in the caller's tenant scope. N3b (generation): turn a `number: {}` field (N1, #6382) into the create-time stamp/placeholder + the issue-step stamp delegate + idempotency, calling this SDK. Depends on #6384. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bf18ecf commit a364d84

3 files changed

Lines changed: 172 additions & 0 deletions

File tree

components/api/api-modules-java/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@
7171
<artifactId>dirigible-components-engine-document</artifactId>
7272
<version>${project.version}</version>
7373
</dependency>
74+
<dependency>
75+
<groupId>org.eclipse.dirigible</groupId>
76+
<artifactId>dirigible-components-engine-numbering</artifactId>
77+
<version>${project.version}</version>
78+
</dependency>
7479
<dependency>
7580
<groupId>org.eclipse.dirigible</groupId>
7681
<artifactId>dirigible-components-api-component</artifactId>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
3+
*
4+
* All rights reserved. This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
9+
*/
10+
package org.eclipse.dirigible.sdk.numbering;
11+
12+
import java.sql.SQLException;
13+
import java.util.Map;
14+
15+
import org.eclipse.dirigible.components.engine.numbering.DocumentNumberService;
16+
import org.eclipse.dirigible.sdk.component.Beans;
17+
18+
/**
19+
* Client SDK for first-class document numbering: allocate the next gap-free number for a series and
20+
* render it through the series' format. Backed by the platform's per-tenant counter store (the same
21+
* store the application shell's Document Numbering settings manage), so hand-written
22+
* {@code custom/} code and the generated stamping share one engine and one sequence.
23+
*
24+
* <p>
25+
* Example: {@code DocumentNumbers.next("SalesInvoice", "SI-{seq:07}", Map.of("year", "2026"))} →
26+
* {@code SI-0000001} (then {@code SI-0000002}, …). The scope map both partitions the counter and
27+
* feeds the format's {@code {year}} / {@code {<Field>}} tokens.
28+
*/
29+
public final class DocumentNumbers {
30+
31+
private DocumentNumbers() {}
32+
33+
/**
34+
* Allocate and format the next number for a series.
35+
*
36+
* @param series the series identity (documents sharing a sequence pass the same series)
37+
* @param format the format template ({@code {seq}} / {@code {seq:0N}} / {@code {series}} / scope
38+
* tokens), or {@code null}/blank for the default {@code {series}-{seq:06}}
39+
* @param scope the resolved scope values partitioning the counter (empty for an unscoped series)
40+
* @return the formatted document number
41+
*/
42+
public static String next(String series, String format, Map<String, String> scope) {
43+
try {
44+
return Beans.get(DocumentNumberService.class)
45+
.next(series, format, scope);
46+
} catch (SQLException e) {
47+
throw new IllegalStateException("Failed to allocate a document number for series [" + series + "]", e);
48+
}
49+
}
50+
51+
/**
52+
* Allocate and format the next number for an unscoped series.
53+
*
54+
* @param series the series identity
55+
* @param format the format template (see {@link #next(String, String, Map)})
56+
* @return the formatted document number
57+
*/
58+
public static String next(String series, String format) {
59+
return next(series, format, Map.of());
60+
}
61+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
3+
*
4+
* All rights reserved. This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
9+
*/
10+
package org.eclipse.dirigible.integration.tests.api;
11+
12+
import static io.restassured.RestAssured.given;
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertTrue;
15+
16+
import java.nio.charset.StandardCharsets;
17+
18+
import org.eclipse.dirigible.components.initializers.synchronizer.SynchronizationProcessor;
19+
import org.eclipse.dirigible.repository.api.IRepository;
20+
import org.eclipse.dirigible.repository.api.IRepositoryStructure;
21+
import org.eclipse.dirigible.tests.base.IntegrationTest;
22+
import org.eclipse.dirigible.tests.framework.restassured.RestAssuredExecutor;
23+
import org.junit.jupiter.api.AfterEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
27+
/**
28+
* End-to-end test for the first-class numbering SDK ({@code sdk.numbering.DocumentNumbers}). Drops
29+
* a client-Java {@code @Controller} that allocates the next number for a series, force-syncs it,
30+
* and asserts over HTTP (in the caller's tenant scope) that successive calls yield a gap-free,
31+
* formatted sequence - exercising the SDK bridge → the platform counter store (engine-numbering).
32+
*/
33+
class NumberingSdkIT extends IntegrationTest {
34+
35+
private static final String PROJECT = "numbering-it";
36+
private static final String CONTROLLER_LOCATION = "/" + PROJECT + "/api/NumberingTestController.java";
37+
private static final String CONTROLLER_PATH = IRepositoryStructure.PATH_REGISTRY_PUBLIC + CONTROLLER_LOCATION;
38+
private static final String ENDPOINT = "/services/java/" + PROJECT + "/api/NumberingTestController/next";
39+
private static final long ASSERTION_TIMEOUT_SECONDS = 30;
40+
41+
@Autowired
42+
private IRepository repository;
43+
44+
@Autowired
45+
private SynchronizationProcessor synchronizationProcessor;
46+
47+
@Autowired
48+
private RestAssuredExecutor restAssuredExecutor;
49+
50+
@Test
51+
void allocatesAGapFreeFormattedSequence() {
52+
repository.createResource(CONTROLLER_PATH, controllerSource().getBytes(StandardCharsets.UTF_8), false, "text/x-java", true);
53+
synchronizationProcessor.forceProcessSynchronizers();
54+
55+
// Both allocations run inside one executor pass (which sets up auth); the assertion is
56+
// RELATIVE (b == a + 1) so a compile-readiness retry that re-runs the whole lambda still holds
57+
// - each pass draws two consecutive numbers rather than depending on an absolute start value.
58+
restAssuredExecutor.execute(() -> {
59+
String a = given().when()
60+
.get(ENDPOINT)
61+
.then()
62+
.statusCode(200)
63+
.extract()
64+
.asString();
65+
String b = given().when()
66+
.get(ENDPOINT)
67+
.then()
68+
.statusCode(200)
69+
.extract()
70+
.asString();
71+
assertTrue(a.matches("T-\\d{4}"), "formatted: " + a);
72+
assertTrue(b.matches("T-\\d{4}"), "formatted: " + b);
73+
assertEquals(Integer.parseInt(a.substring(2)) + 1, Integer.parseInt(b.substring(2)), "gap-free: " + a + " then " + b);
74+
}, ASSERTION_TIMEOUT_SECONDS);
75+
}
76+
77+
@AfterEach
78+
void cleanup() {
79+
if (repository.hasResource(CONTROLLER_PATH)) {
80+
repository.removeResource(CONTROLLER_PATH);
81+
synchronizationProcessor.forceProcessSynchronizers();
82+
}
83+
}
84+
85+
private static String controllerSource() {
86+
return """
87+
package api;
88+
89+
import java.util.Map;
90+
91+
import org.eclipse.dirigible.sdk.http.Controller;
92+
import org.eclipse.dirigible.sdk.http.Get;
93+
import org.eclipse.dirigible.sdk.numbering.DocumentNumbers;
94+
95+
@Controller
96+
public class NumberingTestController {
97+
98+
@Get("/next")
99+
public String next() {
100+
return DocumentNumbers.next("NumberingIT", "T-{seq:04}", Map.of());
101+
}
102+
}
103+
""";
104+
}
105+
106+
}

0 commit comments

Comments
 (0)