Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eighty-donuts-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": patch
---

feat(useSearchParams): Date support
5 changes: 5 additions & 0 deletions .changeset/shaggy-laws-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": patch
---

feat(useSearchParams): Zod codec support
9 changes: 7 additions & 2 deletions packages/runed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,15 @@
],
"peerDependencies": {
"svelte": "^5.7.0",
"@sveltejs/kit": "^2.21.0"
"@sveltejs/kit": "^2.21.0",
"zod": "^4.1.0"
},
"peerDependenciesMeta": {
"@sveltejs/kit": {
"optional": true
},
"zod": {
"optional": true
}
},
"devDependencies": {
Expand All @@ -96,7 +100,8 @@
"tslib": "^2.4.1",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"zod": "^4.1.0"
},
"dependencies": {
"dequal": "^2.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import type { StandardSchemaV1 } from "./use-search-params.svelte.js";

describe("createSearchParamsSchema", () => {
it("applies default values for all schema types", () => {
const testDate = new Date("2023-01-01T00:00:00Z");
const schema = createSearchParamsSchema({
str: { type: "string", default: "hello" },
num: { type: "number", default: 2 },
bool: { type: "boolean", default: true },
date: { type: "date", default: testDate },
arr: { type: "array", default: [1, 2], arrayType: 1 },
obj: { type: "object", default: { a: "b" }, objectType: { a: "" } },
});
Expand All @@ -16,6 +18,7 @@ describe("createSearchParamsSchema", () => {
str: "hello",
num: 2,
bool: true,
date: testDate,
arr: [1, 2],
obj: { a: "b" },
});
Expand All @@ -26,13 +29,16 @@ describe("createSearchParamsSchema", () => {
str: { type: "string", default: "" },
num: { type: "number", default: 0 },
bool: { type: "boolean", default: false },
date: { type: "date", default: new Date() },
arr: { type: "array", default: [], arrayType: 1 },
obj: { type: "object", default: {}, objectType: { a: "" } },
});
const testDate = new Date("2023-06-15T10:30:00.000Z");
const input = {
str: 123,
num: "5",
bool: "true",
date: testDate,
arr: [3, 4],
obj: { c: 4 },
} as unknown;
Expand All @@ -41,17 +47,19 @@ describe("createSearchParamsSchema", () => {
str: "123",
num: 5,
bool: true,
date: testDate,
arr: [3, 4],
obj: { c: 4 },
});
});

it("returns issues for invalid number, array, and object inputs", () => {
it("returns issues for invalid number, array, date, and object inputs", () => {
const schema = createSearchParamsSchema({
foo: { type: "string" },
bar: { type: "number" },
baz: { type: "array" },
qux: { type: "object" },
date: { type: "date" },
});

// Invalid input
Expand All @@ -60,6 +68,7 @@ describe("createSearchParamsSchema", () => {
bar: "not-a-number",
baz: "not-array",
qux: "not-object",
date: "invalid-date-string",
}) as StandardSchemaV1.FailureResult;

expect("issues" in invalidResult).toBe(true);
Expand All @@ -78,6 +87,10 @@ describe("createSearchParamsSchema", () => {
message: expect.stringContaining("Invalid object"),
path: ["qux"],
}),
expect.objectContaining({
message: expect.stringContaining("Invalid date"),
path: ["date"],
}),
])
);
});
Expand All @@ -104,13 +117,15 @@ describe("createSearchParamsSchema", () => {
bar: { type: "number" },
baz: { type: "array" },
qux: { type: "object" },
date: { type: "date" },
});
const emptyResult = schema["~standard"].validate({});
expect("value" in emptyResult && emptyResult.value).toEqual({
foo: null,
bar: null,
baz: null,
qux: null,
date: null,
});
});

Expand Down Expand Up @@ -149,4 +164,98 @@ describe("createSearchParamsSchema", () => {
count: 456, // converted to number
});
});

describe("Date type support", () => {
it("validates Date objects correctly", () => {
const schema = createSearchParamsSchema({
startDate: { type: "date", default: new Date("2023-01-01T00:00:00Z") },
endDate: { type: "date", default: new Date("2023-12-31T23:59:59Z") },
});

// Test with Date objects
const result = schema["~standard"].validate({
startDate: new Date("2023-06-15T10:30:00Z"),
endDate: new Date("2023-06-20T18:00:00Z"),
});

expect(result).toHaveProperty("value");
if ("value" in result) {
expect(result.value.startDate).toBeInstanceOf(Date);
expect(result.value.endDate).toBeInstanceOf(Date);
expect(result.value.startDate.toISOString()).toBe("2023-06-15T10:30:00.000Z");
expect(result.value.endDate.toISOString()).toBe("2023-06-20T18:00:00.000Z");
}
});

it("stores dateFormat metadata in schema", () => {
const schema = createSearchParamsSchema({
birthDate: { type: "date", default: new Date("1990-01-15T00:00:00Z"), dateFormat: "date" },
createdAt: {
type: "date",
default: new Date("2023-01-01T00:00:00Z"),
dateFormat: "datetime",
},
updatedAt: { type: "date", default: new Date("2023-01-01T00:00:00Z") }, // no format specified
});

// Check that dateFormat metadata is stored
const schemaWithMetadata = schema as typeof schema & {
__dateFormats?: Record<string, "date" | "datetime">;
};
expect(schemaWithMetadata.__dateFormats).toBeDefined();
expect(schemaWithMetadata.__dateFormats?.birthDate).toBe("date");
expect(schemaWithMetadata.__dateFormats?.createdAt).toBe("datetime");
expect(schemaWithMetadata.__dateFormats?.updatedAt).toBeUndefined(); // not specified
});

it("validates ISO8601 strings correctly (simulating URL parameters)", () => {
const schema = createSearchParamsSchema({
startDate: { type: "date", default: new Date("2023-01-01T00:00:00Z") },
});

// Test with ISO8601 string (simulating URL parameter)
const result = schema["~standard"].validate({
startDate: "2023-06-15T10:30:00.000Z",
});

expect(result).toHaveProperty("value");
if ("value" in result) {
expect(result.value.startDate).toBeInstanceOf(Date);
expect(result.value.startDate.toISOString()).toBe("2023-06-15T10:30:00.000Z");
}
});

it("rejects invalid date strings", () => {
const schema = createSearchParamsSchema({
startDate: { type: "date", default: new Date("2023-01-01T00:00:00Z") },
});

// Test with invalid date string
const result = schema["~standard"].validate({
startDate: "invalid-date-string",
});

expect(result).toHaveProperty("issues");
if ("issues" in result && result.issues) {
expect(result.issues.length).toBeGreaterThan(0);
expect(result.issues[0]?.message).toContain("Invalid date");
}
});

it("uses default Date values when no input provided", () => {
const defaultDate = new Date("2023-01-01T00:00:00Z");
const schema = createSearchParamsSchema({
startDate: { type: "date", default: defaultDate },
});

// Test with empty object (should use defaults)
const result = schema["~standard"].validate({});

expect(result).toHaveProperty("value");
if ("value" in result) {
expect(result.value.startDate).toBeInstanceOf(Date);
expect(result.value.startDate.toISOString()).toBe(defaultDate.toISOString());
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("extractParamValues", () => {
it("converts numeric strings to numbers when field is in numberFields", () => {
const params = new URLSearchParams("page=5&limit=10&code=123");
const numberFields = new Set(["page", "limit"]);
const result = extractParamValues(params, numberFields);
const result = extractParamValues(params, { numberFields });

expect(result.page).toBe(5);
expect(typeof result.page).toBe("number");
Expand All @@ -27,23 +27,23 @@ describe("extractParamValues", () => {
it("handles negative numbers", () => {
const params = new URLSearchParams("temperature=-5");
const numberFields = new Set(["temperature"]);
const result = extractParamValues(params, numberFields);
const result = extractParamValues(params, { numberFields });

expect(result.temperature).toBe(-5);
});

it("handles decimal numbers", () => {
const params = new URLSearchParams("price=19.99");
const numberFields = new Set(["price"]);
const result = extractParamValues(params, numberFields);
const result = extractParamValues(params, { numberFields });

expect(result.price).toBe(19.99);
});

it("keeps strings as strings when not in numberFields", () => {
const params = new URLSearchParams("name=John&id=12345");
const numberFields = new Set([]);
const result = extractParamValues(params, numberFields);
const result = extractParamValues(params, { numberFields });

expect(result.name).toBe("John");
expect(typeof result.name).toBe("string");
Expand Down Expand Up @@ -125,7 +125,7 @@ describe("extractParamValues", () => {
it("should split comma-separated values ONLY for array fields (currently fails)", () => {
const params = new URLSearchParams("name=Smith, John&tags=tag1,tag2,tag3");

const result = extractParamValues(params, new Set(), new Set(["tags"]));
const result = extractParamValues(params, { arrayFields: new Set(["tags"]) });

expect(result.name).toBe("Smith, John");
expect(typeof result.name).toBe("string");
Expand All @@ -134,7 +134,7 @@ describe("extractParamValues", () => {

it("should NOT treat CSV-like data as array by default", () => {
const params = new URLSearchParams("data=column1,column2,column3");
const result = extractParamValues(params, new Set(), new Set());
const result = extractParamValues(params);

expect(result.data).toBe("column1,column2,column3");
expect(typeof result.data).toBe("string");
Expand Down Expand Up @@ -200,7 +200,7 @@ describe("extractParamValues", () => {
it("processes multiple different types correctly", () => {
const params = new URLSearchParams('page=5&active=true&tags=["a","b"]&name=John Doe&empty=');
const numberFields = new Set(["page"]);
const result = extractParamValues(params, numberFields);
const result = extractParamValues(params, { numberFields });

expect(result.page).toBe(5);
expect(result.active).toBe(true);
Expand All @@ -212,7 +212,7 @@ describe("extractParamValues", () => {
it("name with comma should NOT be split even when number field specified", () => {
const params = new URLSearchParams("page=3&name=Smith, John");
const numberFields = new Set(["page"]);
const result = extractParamValues(params, numberFields);
const result = extractParamValues(params, { numberFields });

expect(result.page).toBe(3);
// Expected: name should remain as string
Expand All @@ -234,7 +234,7 @@ describe("extractParamValues", () => {
it("splits comma-separated values ONLY when field is marked as array", () => {
const params = new URLSearchParams("tags=red,%20blue,%20%20green");
const arrayFields = new Set(["tags"]);
const result = extractParamValues(params, new Set(), arrayFields);
const result = extractParamValues(params, { arrayFields });

expect(result.tags).toEqual(["red", " blue", " green"]);
expect(Array.isArray(result.tags)).toBe(true);
Expand All @@ -243,7 +243,7 @@ describe("extractParamValues", () => {
it("splits numeric comma-separated values into array only for array fields", () => {
const params = new URLSearchParams("ids=1,2,3,4,5");
const arrayFields = new Set(["ids"]);
const result = extractParamValues(params, new Set(), arrayFields);
const result = extractParamValues(params, { arrayFields });

expect(result.ids).toEqual(["1", "2", "3", "4", "5"]);
expect(Array.isArray(result.ids)).toBe(true);
Expand Down
Loading