appsync-scalars 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 William Goodwin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,123 @@
1
- # Temporary Holding Version
1
+ # appsync-scalars
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Validates the AWS AppSync date and time scalars (`AWSDate`, `AWSTime`, `AWSDateTime` and `AWSTimestamp`) with one Rust implementation, available in Rust, Python, JavaScript and Go.
4
+
5
+ The rules come from results recorded from a live AppSync API, which differ from AWS's written descriptions. See [What AppSync accepts](#what-appsync-accepts).
6
+
7
+ ## Usage
8
+
9
+ ### Rust
10
+
11
+ ```rust
12
+ use appsync_scalars::{is_aws_date, validate_aws_datetime};
13
+
14
+ assert!(is_aws_date("1970-01-01+05:30"));
15
+
16
+ // Err: invalid AWSDateTime: time zone offset is required
17
+ validate_aws_datetime("2021-01-11T12:30:24").unwrap_err();
18
+ ```
19
+
20
+ ### Python
21
+
22
+ ```python
23
+ from appsync_scalars import is_aws_date, validate_aws_datetime
24
+
25
+ is_aws_date("1970-01-01+05:30") # True
26
+ validate_aws_datetime("2021-01-11T12:30:24") # raises ValueError
27
+ ```
28
+
29
+ The core package has no dependencies. Pydantic field types are an optional extra, installed with `pip install "appsync-scalars[pydantic]"`:
30
+
31
+ ```python
32
+ from pydantic import BaseModel
33
+ from appsync_scalars.pydantic import AWSDate, AWSDateTime, AWSTimestamp
34
+
35
+ class Event(BaseModel):
36
+ day: AWSDate
37
+ starts_at: AWSDateTime
38
+ created: AWSTimestamp
39
+ ```
40
+
41
+ Fields keep the exact `str` or `int` they were given, because Python's `datetime` cannot hold negative years and keeps only microseconds. They are also strict, so `"123"` is not converted to an integer for `AWSTimestamp`.
42
+
43
+ ### JavaScript and TypeScript
44
+
45
+ ```js
46
+ import { isAWSDate, validateAWSDateTime } from "appsync-scalars";
47
+
48
+ isAWSDate("1970-01-01+05:30"); // true
49
+ validateAWSDateTime("2021-01-11T12:30:24"); // throws AppSyncScalarError
50
+ ```
51
+
52
+ ### Go
53
+
54
+ ```go
55
+ import appsyncscalars "github.com/wbgoodwin/appsync-scalars/bindings/go"
56
+
57
+ appsyncscalars.IsAWSDate("1970-01-01+05:30") // true
58
+ err := appsyncscalars.ValidateAWSDateTime("2021-01-11T12:30:24") // *appsyncscalars.Error
59
+ ```
60
+
61
+ `AWSTimestamp` functions take the integer's text in Go and Rust, and an integer in Python and JavaScript.
62
+
63
+ ## What AppSync accepts
64
+
65
+ AppSync's descriptions of these scalars are accurate for ordinary values. It also accepts and rejects some values they don't mention, and this library follows AppSync:
66
+
67
+ | Value | Example | AppSync's behavior |
68
+ |---|---|---|
69
+ | Lowercase `z` and `t` | `2021-01-11t12:30:24z` | Accepted |
70
+ | Empty fraction | `12:00:34.` | Accepted |
71
+ | Signed years of 5 to 10 digits | `+12021-01-11`, `-12021-01-11` | Accepted, up to 999,999,999 either way |
72
+ | `+` with a 4-digit year | `+2021-01-11` | Rejected |
73
+ | Negative year zero | `-0000-01-11` | Rejected |
74
+ | Hours-only offset | `+05` | Accepted by `AWSDateTime`, rejected by `AWSDate` and `AWSTime` |
75
+ | Offset beyond 18 hours | `+18:01` | Rejected by `AWSDateTime`, accepted by `AWSDate` and `AWSTime` up to `+23:59:59` |
76
+
77
+ `AWSTimestamp` follows its documented limits, -31557014167219200 to 31556889864403199.
78
+
79
+ AppSync gave the same verdict for a GraphQL variable and for resolver output on every recorded value. Inline literals differ in one way: AppSync wraps oversized integers, so `18446744073709551616` written in a query reaches the resolver as `0`. This library calls that value invalid, as AppSync does for variables and output.
80
+
81
+ ## How it fits together
82
+
83
+ | Path | Purpose |
84
+ |---|---|
85
+ | [`crates/appsync-scalars`](https://github.com/wbgoodwin/appsync-scalars/blob/main/crates/appsync-scalars/src/lib.rs) | The validator: Rust with no dependencies |
86
+ | [`crates/appsync-scalars-wasm`](https://github.com/wbgoodwin/appsync-scalars/blob/main/crates/appsync-scalars-wasm/src/lib.rs) | The WebAssembly module, about 4 KB, shared by JavaScript and Go |
87
+ | [`bindings/python`](https://github.com/wbgoodwin/appsync-scalars/blob/main/bindings/python/pyproject.toml) | Native extension built with PyO3 and maturin, plus the Pydantic types |
88
+ | [`bindings/js`](https://github.com/wbgoodwin/appsync-scalars/blob/main/bindings/js/index.js) | npm package that embeds the module, so it has no install step or platform-specific builds |
89
+ | [`bindings/go`](https://github.com/wbgoodwin/appsync-scalars/blob/main/bindings/go/appsyncscalars.go) | Go module that runs the embedded module through wazero, with no cgo |
90
+ | [`conformance/smoke.json`](https://github.com/wbgoodwin/appsync-scalars/blob/main/conformance/smoke.json) | Test cases every language runs |
91
+ | [`conformance/`](https://github.com/wbgoodwin/appsync-scalars/blob/main/conformance/README.md) | The live AppSync test suite |
92
+
93
+ Other languages with a WebAssembly runtime can load the same module. Its interface is described in [`crates/appsync-scalars-wasm/src/lib.rs`](https://github.com/wbgoodwin/appsync-scalars/blob/main/crates/appsync-scalars-wasm/src/lib.rs).
94
+
95
+ ## Development
96
+
97
+ You need `rustup`, which installs the pinned Rust version, plus `uv`, Node.js 20 or later, and Go 1.25 or later.
98
+
99
+ Run every language's tests:
100
+
101
+ ```bash
102
+ scripts/test-all.sh
103
+ ```
104
+
105
+ After changing the validator, rebuild the WebAssembly module and commit the Go copy, because `go get` only sees files in the repository:
106
+
107
+ ```bash
108
+ scripts/build-wasm.sh
109
+ ```
110
+
111
+ ## Releasing
112
+
113
+ GitHub Actions runs the tests, and builds and tests a wheel on Linux, macOS and Windows, for every pull request and push to `main`.
114
+
115
+ To release, set the new version in `crates/appsync-scalars/Cargo.toml`, `bindings/python/pyproject.toml` and `bindings/js/package.json`, merge to `main`, then push a tag:
116
+
117
+ ```bash
118
+ git tag v1.2.3 && git push origin v1.2.3
119
+ ```
120
+
121
+ The workflow repeats every check, publishes to crates.io, PyPI and npm, and adds the `bindings/go/v1.2.3` tag Go needs. It skips anything already on a registry, so rerunning a partly failed release is safe.
122
+
123
+ Publishing stores no tokens. Each registry needs a trusted publisher for this repository, with the workflow `ci.yml` and the environment `release`. PyPI can set one up before the first release. crates.io and npm need the first version published by hand, and npm's publisher must allow `npm publish`.
package/index.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type AppSyncScalar = "AWSDate" | "AWSTime" | "AWSDateTime" | "AWSTimestamp";
2
+
3
+ export class AppSyncScalarError extends Error {
4
+ readonly scalar: AppSyncScalar;
5
+ readonly reason: string;
6
+ constructor(scalar: AppSyncScalar, reason: string);
7
+ }
8
+
9
+ export function isAWSDate(value: unknown): value is string;
10
+ export function isAWSTime(value: unknown): value is string;
11
+ export function isAWSDateTime(value: unknown): value is string;
12
+ export function isAWSTimestamp(value: unknown): value is number | bigint;
13
+
14
+ /** @throws {AppSyncScalarError} if the value is invalid. */
15
+ export function validateAWSDate(value: unknown): asserts value is string;
16
+ /** @throws {AppSyncScalarError} if the value is invalid. */
17
+ export function validateAWSTime(value: unknown): asserts value is string;
18
+ /** @throws {AppSyncScalarError} if the value is invalid. */
19
+ export function validateAWSDateTime(value: unknown): asserts value is string;
20
+ /** @throws {AppSyncScalarError} if the value is invalid. */
21
+ export function validateAWSTimestamp(value: unknown): asserts value is number | bigint;
package/index.js ADDED
@@ -0,0 +1,98 @@
1
+ import { WASM_BASE64 } from "./wasm.generated.js";
2
+
3
+ const SCALAR_CODES = { AWSDate: 0, AWSTime: 1, AWSDateTime: 2, AWSTimestamp: 3 };
4
+
5
+ export class AppSyncScalarError extends Error {
6
+ constructor(scalar, reason) {
7
+ super(`invalid ${scalar}: ${reason}`);
8
+ this.name = "AppSyncScalarError";
9
+ this.scalar = scalar;
10
+ this.reason = reason;
11
+ }
12
+ }
13
+
14
+ const encoder = new TextEncoder();
15
+ const decoder = new TextDecoder();
16
+ let module;
17
+
18
+ // Loads on first use, so importing the package costs nothing until a value is checked.
19
+ function load() {
20
+ if (!module) {
21
+ const bytes = Uint8Array.from(atob(WASM_BASE64), (char) => char.charCodeAt(0));
22
+ const { exports } = new WebAssembly.Instance(new WebAssembly.Module(bytes));
23
+ module = {
24
+ exports,
25
+ inputPtr: exports.appsync_input_ptr(),
26
+ inputCapacity: exports.appsync_input_capacity(),
27
+ };
28
+ }
29
+ return module;
30
+ }
31
+
32
+ /** Returns null if `text` is valid, or the reason it is not. */
33
+ function reasonFor(scalar, text) {
34
+ const { exports, inputPtr, inputCapacity } = load();
35
+ const input = new Uint8Array(exports.memory.buffer, inputPtr, inputCapacity);
36
+ const { read, written } = encoder.encodeInto(text, input);
37
+ // Text that did not fit is reported as too long, which the module always rejects.
38
+ const length = read < text.length ? inputCapacity + 1 : written;
39
+ const code = exports.appsync_validate(SCALAR_CODES[scalar], length);
40
+ if (code === 0) {
41
+ return null;
42
+ }
43
+ const message = new Uint8Array(
44
+ exports.memory.buffer,
45
+ exports.appsync_reason_ptr(code),
46
+ exports.appsync_reason_len(code),
47
+ );
48
+ return decoder.decode(message);
49
+ }
50
+
51
+ function checkText(scalar, value) {
52
+ return typeof value === "string" ? reasonFor(scalar, value) : "expected a string";
53
+ }
54
+
55
+ function checkTimestamp(value) {
56
+ if (typeof value === "bigint" || Number.isInteger(value)) {
57
+ return reasonFor("AWSTimestamp", String(value));
58
+ }
59
+ return "expected an integer";
60
+ }
61
+
62
+ function throwIfInvalid(scalar, reason) {
63
+ if (reason !== null) {
64
+ throw new AppSyncScalarError(scalar, reason);
65
+ }
66
+ }
67
+
68
+ export function isAWSDate(value) {
69
+ return checkText("AWSDate", value) === null;
70
+ }
71
+
72
+ export function isAWSTime(value) {
73
+ return checkText("AWSTime", value) === null;
74
+ }
75
+
76
+ export function isAWSDateTime(value) {
77
+ return checkText("AWSDateTime", value) === null;
78
+ }
79
+
80
+ export function isAWSTimestamp(value) {
81
+ return checkTimestamp(value) === null;
82
+ }
83
+
84
+ export function validateAWSDate(value) {
85
+ throwIfInvalid("AWSDate", checkText("AWSDate", value));
86
+ }
87
+
88
+ export function validateAWSTime(value) {
89
+ throwIfInvalid("AWSTime", checkText("AWSTime", value));
90
+ }
91
+
92
+ export function validateAWSDateTime(value) {
93
+ throwIfInvalid("AWSDateTime", checkText("AWSDateTime", value));
94
+ }
95
+
96
+ export function validateAWSTimestamp(value) {
97
+ throwIfInvalid("AWSTimestamp", checkTimestamp(value));
98
+ }
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "appsync-scalars",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Validates AWS AppSync date and time scalar values.",
5
+ "keywords": ["appsync", "aws", "graphql", "validation", "iso8601"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/wbgoodwin/appsync-scalars.git",
10
+ "directory": "bindings/js"
11
+ },
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./index.d.ts",
16
+ "default": "./index.js"
17
+ }
18
+ },
19
+ "types": "./index.d.ts",
20
+ "files": ["index.js", "index.d.ts", "wasm.generated.js"],
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "scripts": {
26
+ "test": "node --test",
27
+ "prepack": "cp ../../README.md . && ../../scripts/build-wasm.sh"
28
+ }
29
+ }
@@ -0,0 +1,2 @@
1
+ // Generated by scripts/build-wasm.sh. Do not edit.
2
+ export const WASM_BASE64 = "AGFzbQEAAAABJgdgAn9/AGAAAX9gAX8Bf2ACf38Bf2ADf39/AGAAAGAFf35+fn4AAw4NAAEBAgIDAgQCAgQFBgUDAQARBgkBfwFBgIDAAAsHdAYGbWVtb3J5AgAWYXBwc3luY19pbnB1dF9jYXBhY2l0eQABEWFwcHN5bmNfaW5wdXRfcHRyAAISYXBwc3luY19yZWFzb25fbGVuAAMSYXBwc3luY19yZWFzb25fcHRyAAQQYXBwc3luY192YWxpZGF0ZQAFCosiDVYBAn9BACECAkACQCABRQ0AIAFBDEsNACABQf//v4AAaiIBRQ0BIAEtAAAiAUECdCgC8ITAgAAhAiABLQDkhMCAACEDDAELCyAAIAM2AgQgACACNgIACwUAQYAICwgAQaCFwIAAC0IBAn8jgICAgABBEGsiASSAgICAACABQQhqIAAQgICAgAAgASgCCCEAIAEoAgwhAiABQRBqJICAgIAAIAJBACAAGws2AQF/I4CAgIAAQRBrIgEkgICAgAAgAUEIaiAAEICAgIAAIAEoAgghACABQRBqJICAgIAAIAAL6g0CCH8CfiOAgICAAEHAAGsiAiSAgICAAEEBIQMCQCAAQQNLDQAgAUGACEsNAAJAIAFFDQBBACABQXlqIgQgBCABSxshBUGjhcCAAEF8cUGghcCAAGshBkEAIQQDQAJAAkACQAJAIAQtAKCFwIAAIgfAIghBAEgNACAGIARrQQNxDQEgBCAFTw0CA0AgBEGkhcCAAGooAgAgBEGghcCAAGooAgByQYCBgoR4cQ0DIARBCGoiBCAFSQ0ADAMLC0EBIQMCQAJAAkACQAJAAkACQAJAAkAgBy0A44LAgABBfmoOAwACAQ4LQQEhAyAEQQFqIgQgAU8NDSAELACghcCAAEG/f0wNBwwNC0EBIQMgBEEBaiIJIAFPDQwgCSwAoIXAgAAhCSAHQZB+ag4FAgEBAQMBC0EBIQMgBEEBaiIJIAFPDQsgCSwAoIXAgAAhCQJAAkACQCAHQeABRg0AIAdB7QFGDQEgCEEfakH/AXFBDEkNAiAIQX5xQW5HDQ4gCUFASA0HDA4LIAlBYHFBoH9GDQYMDQsgCUGff0oNDAwFCyAJQUBIDQQMCwsgCEEPakH/AXFBAksNCiAJQUBIDQIMCgsgCUHwAGpB/wFxQTBJDQEMCQsgCUGPf0oNCAsgBEECaiIHIAFPDQcgBywAoIXAgABBv39KDQcgBEEDaiIEIAFPDQcgBCwAoIXAgABBv39KDQcMAQsgBEECaiIEIAFPDQYgBCwAoIXAgABBv39KDQYLIARBAWohBAwCCyAEQQFqIQQMAQsgBCABTw0AA0AgBEGghcCAAGosAABBAEgNASABIARBAWoiBEcNAAwDCwsgBCABSQ0ACwsCQAJAAkACQAJAAkACQAJAIAAOBAABAgMACyACIAE2AiwgAkGghcCAADYCKCACQShqEIaAgIAAIgNB/wFxQf8BRw0EIAJBMGogAkEoakEAEIeAgIAAIAIpAzBCAlENA0EAQX8gAigCLBshAwwECyACIAE2AiwgAkGghcCAADYCKCACQShqEIiAgIAAIgNB/wFxQf8BRw0DIAJBMGogAkEoakEAEIeAgIAAIAIpAzBCAlENAkEAQX8gAigCLBshAwwDCyACIAE2AiwgAkGghcCAADYCKCACQShqEIaAgIAAIgNB/wFxQf8BRw0CQQAhAyACKAIsIgRFDQIgAigCKCIBLQAAQSByQfQARw0CIAIgBEF/ajYCLCACIAFBAWo2AiggAkEoahCIgICAACIDQf8BcUH/AUcNAiACQTBqIAJBKGpBARCHgICAACACKQMwIgpCAlENAUEAIQMgAigCLA0CQQtBfyACKQM4QqD6A1YbQQggCqdBAXEbIQMMAgtBACEDIAEgAUEAR0EALQCghcCAAEEtRnEiB2siBUUNA0GhhcCAAEGghcCAACAHGyEAQQAhBAJAAkADQCAFIARGDQEgACAEaiEDIARBAWohBCADLQAAQUZqQf8BcUH2AU8NAAwCCwsCQCAFQQFGDQAgB0GghcCAAGotAABB/wFxQTBGDQELQQkhAwJAAkACQCABDgIHAAELQQkhA0EALQCghcCAACIFQVVqDgMGAQYBC0EALQCghcCAACEFC0GghcCAACEEAkACQAJAAkACQAJAIAVB/wFxQVVqDgMBAgACCyABQRFJDQJBASEEQgAhCgNAIAJBEGogCiAKQj+HQgpCABCMgICAACACKQMYIAIpAxAiCkI/h1INBUEJIQMgBEGghcCAAGotAABBUGoiBUEJSw0KIAogBa0iC1MgCiALfSIKQgBTcw0KIAEgBEEBaiIERw0ADAQLCyABQX9qIQFBoYXAgAAhBAsC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";