Implement Workstream F1: me.get and me.setupProfile procedures
- Add me.get procedure returning user profile with needsSetup flag - Add me.setupProfile procedure for initial profile setup after signup - Add nonEmptyString/optionalString schema helpers with tests - Use Web Crypto API (SubtleCrypto) for Cloudflare Workers compatibility - Use @formatjs/intl-durationformat for duration formatting - Remove node:crypto dependency from crypto utilities Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@ import { configs } from "@macalinao/eslint-config";
|
||||
|
||||
export default [
|
||||
...configs.fast,
|
||||
{
|
||||
ignores: ["**/*.test.ts"],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "bun test",
|
||||
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
||||
"lint": "eslint . --cache"
|
||||
},
|
||||
|
||||
81
packages/api-contract/src/schemas/common.test.ts
Normal file
81
packages/api-contract/src/schemas/common.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { nonEmptyString, optionalString } from "./common.js";
|
||||
|
||||
describe("nonEmptyString", () => {
|
||||
const schema = nonEmptyString(100);
|
||||
|
||||
test("accepts valid non-empty string", () => {
|
||||
expect(schema.parse("hello")).toBe("hello");
|
||||
});
|
||||
|
||||
test("trims whitespace", () => {
|
||||
expect(schema.parse(" hello ")).toBe("hello");
|
||||
});
|
||||
|
||||
test("rejects empty string", () => {
|
||||
expect(() => schema.parse("")).toThrow();
|
||||
});
|
||||
|
||||
test("rejects whitespace-only string", () => {
|
||||
expect(() => schema.parse(" ")).toThrow();
|
||||
});
|
||||
|
||||
test("rejects string exceeding max length", () => {
|
||||
const shortSchema = nonEmptyString(5);
|
||||
expect(() => shortSchema.parse("123456")).toThrow();
|
||||
});
|
||||
|
||||
test("accepts string at max length", () => {
|
||||
const shortSchema = nonEmptyString(5);
|
||||
expect(shortSchema.parse("12345")).toBe("12345");
|
||||
});
|
||||
|
||||
test("works without max length", () => {
|
||||
const noMaxSchema = nonEmptyString();
|
||||
expect(noMaxSchema.parse("a".repeat(1000))).toBe("a".repeat(1000));
|
||||
});
|
||||
});
|
||||
|
||||
describe("optionalString", () => {
|
||||
const schema = optionalString(200);
|
||||
|
||||
test("accepts valid non-empty string", () => {
|
||||
expect(schema.parse("hello")).toBe("hello");
|
||||
});
|
||||
|
||||
test("trims whitespace", () => {
|
||||
expect(schema.parse(" hello ")).toBe("hello");
|
||||
});
|
||||
|
||||
test("transforms empty string to undefined", () => {
|
||||
expect(schema.parse("")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("transforms whitespace-only string to undefined", () => {
|
||||
expect(schema.parse(" ")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("accepts undefined input", () => {
|
||||
expect(schema.parse(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("rejects string exceeding max length", () => {
|
||||
const shortSchema = optionalString(5);
|
||||
expect(() => shortSchema.parse("123456")).toThrow();
|
||||
});
|
||||
|
||||
test("accepts string at max length", () => {
|
||||
const shortSchema = optionalString(5);
|
||||
expect(shortSchema.parse("12345")).toBe("12345");
|
||||
});
|
||||
|
||||
test("works without max length", () => {
|
||||
const noMaxSchema = optionalString();
|
||||
expect(noMaxSchema.parse("a".repeat(1000))).toBe("a".repeat(1000));
|
||||
});
|
||||
|
||||
test("transforms empty to undefined without max length", () => {
|
||||
const noMaxSchema = optionalString();
|
||||
expect(noMaxSchema.parse("")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,25 @@ import {
|
||||
} from "libphonenumber-js";
|
||||
import * as z from "zod";
|
||||
|
||||
/**
|
||||
* Non-empty string schema - trims whitespace and ensures at least 1 char
|
||||
* Use for required text fields that shouldn't be blank
|
||||
*/
|
||||
export const nonEmptyString = (maxLength?: number) => {
|
||||
const base = z.string().trim().min(1);
|
||||
return maxLength ? base.max(maxLength) : base;
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional non-empty string - trims and converts empty/whitespace to undefined
|
||||
* Use for optional text fields where blank should be treated as not provided
|
||||
*/
|
||||
export const optionalString = (maxLength?: number) => {
|
||||
const base = z.string().trim();
|
||||
const withMax = maxLength ? base.max(maxLength) : base;
|
||||
return withMax.optional().transform((v) => (v === "" ? undefined : v));
|
||||
};
|
||||
|
||||
/**
|
||||
* Email schema - validates email format and transforms to lowercase
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from "zod";
|
||||
import { phoneSchema } from "./common.js";
|
||||
import { nonEmptyString, optionalString, phoneSchema } from "./common.js";
|
||||
|
||||
/**
|
||||
* User profile schema
|
||||
@@ -22,8 +22,8 @@ export const userProfileSchema = z.object({
|
||||
* Used after signup to collect profile information
|
||||
*/
|
||||
export const setupProfileInputSchema = z.object({
|
||||
displayName: z.string().min(1).max(100),
|
||||
fullName: z.string().max(200).optional(),
|
||||
displayName: nonEmptyString(100),
|
||||
fullName: optionalString(200),
|
||||
phoneNumber: phoneSchema,
|
||||
});
|
||||
|
||||
@@ -32,10 +32,10 @@ export const setupProfileInputSchema = z.object({
|
||||
* All fields optional for partial updates
|
||||
*/
|
||||
export const updateProfileInputSchema = z.object({
|
||||
displayName: z.string().min(1).max(100).optional(),
|
||||
fullName: z.string().max(200).optional(),
|
||||
displayName: nonEmptyString(100).optional(),
|
||||
fullName: optionalString(200),
|
||||
phoneNumber: phoneSchema,
|
||||
avatarUrl: z.string().optional(),
|
||||
avatarUrl: optionalString(),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -95,5 +95,5 @@ export const deviceOutputSchema = z.object({
|
||||
* Used to name and trust the current device
|
||||
*/
|
||||
export const trustDeviceInputSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
name: nonEmptyString(100),
|
||||
});
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedDeclarations": false
|
||||
}
|
||||
},
|
||||
"exclude": ["**/*.test.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user