feat: items page

This commit is contained in:
2025-03-14 14:14:35 -04:00
parent f58b3cc18d
commit 50c8d18df9
17 changed files with 2384 additions and 54 deletions

View File

@ -18,5 +18,7 @@
--color-subtext-1: #bac2de;
--color-text: #cdd6f4;
--color-sky: #89dceb;
--color-red: #f38ba8;
}

View File

@ -0,0 +1,296 @@
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
// @generated from file item/v1/item.proto (package item.v1, syntax proto3)
/* eslint-disable */
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1";
import type { Timestamp } from "@bufbuild/protobuf/wkt";
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file item/v1/item.proto.
*/
export const file_item_v1_item: GenFile = /*@__PURE__*/
fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEinAEKBEl0ZW0SDwoCaWQYASABKA1IAIgBARIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg0KBXByaWNlGAQgASgCEhAKCHF1YW50aXR5GAUgASgNEi4KBWFkZGVkGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBiAEBQgUKA19pZEIICgZfYWRkZWQiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKA0iLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKA1IA4gBARITCgZvZmZzZXQYBSABKA1IBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAQiMAoRQ3JlYXRlSXRlbVJlcXVlc3QSGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIxChJDcmVhdGVJdGVtUmVzcG9uc2USGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIwChFVcGRhdGVJdGVtUmVxdWVzdBIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIjEKElVwZGF0ZUl0ZW1SZXNwb25zZRIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgNIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKdAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL2l0ZW0vdjE7aXRlbXYxogIDSVhYqgIHSXRlbS5WMcoCB0l0ZW1cVjHiAhNJdGVtXFYxXEdQQk1ldGFkYXRh6gIISXRlbTo6VjFiBnByb3RvMw", [file_google_protobuf_timestamp]);
/**
* @generated from message item.v1.Item
*/
export type Item = Message<"item.v1.Item"> & {
/**
* @generated from field: optional uint32 id = 1;
*/
id?: number;
/**
* @generated from field: string name = 2;
*/
name: string;
/**
* @generated from field: string description = 3;
*/
description: string;
/**
* @generated from field: float price = 4;
*/
price: number;
/**
* @generated from field: uint32 quantity = 5;
*/
quantity: number;
/**
* @generated from field: optional google.protobuf.Timestamp added = 6;
*/
added?: Timestamp;
};
/**
* Describes the message item.v1.Item.
* Use `create(ItemSchema)` to create a new message.
*/
export const ItemSchema: GenMessage<Item> = /*@__PURE__*/
messageDesc(file_item_v1_item, 0);
/**
* @generated from message item.v1.GetItemRequest
*/
export type GetItemRequest = Message<"item.v1.GetItemRequest"> & {
/**
* @generated from field: uint32 id = 1;
*/
id: number;
};
/**
* Describes the message item.v1.GetItemRequest.
* Use `create(GetItemRequestSchema)` to create a new message.
*/
export const GetItemRequestSchema: GenMessage<GetItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 1);
/**
* @generated from message item.v1.GetItemResponse
*/
export type GetItemResponse = Message<"item.v1.GetItemResponse"> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.GetItemResponse.
* Use `create(GetItemResponseSchema)` to create a new message.
*/
export const GetItemResponseSchema: GenMessage<GetItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 2);
/**
* @generated from message item.v1.GetItemsRequest
*/
export type GetItemsRequest = Message<"item.v1.GetItemsRequest"> & {
/**
* @generated from field: optional google.protobuf.Timestamp start = 1;
*/
start?: Timestamp;
/**
* @generated from field: optional google.protobuf.Timestamp end = 2;
*/
end?: Timestamp;
/**
* @generated from field: optional string filter = 3;
*/
filter?: string;
/**
* @generated from field: optional uint32 limit = 4;
*/
limit?: number;
/**
* @generated from field: optional uint32 offset = 5;
*/
offset?: number;
};
/**
* Describes the message item.v1.GetItemsRequest.
* Use `create(GetItemsRequestSchema)` to create a new message.
*/
export const GetItemsRequestSchema: GenMessage<GetItemsRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 3);
/**
* @generated from message item.v1.GetItemsResponse
*/
export type GetItemsResponse = Message<"item.v1.GetItemsResponse"> & {
/**
* @generated from field: repeated item.v1.Item items = 1;
*/
items: Item[];
/**
* @generated from field: uint64 count = 2;
*/
count: bigint;
};
/**
* Describes the message item.v1.GetItemsResponse.
* Use `create(GetItemsResponseSchema)` to create a new message.
*/
export const GetItemsResponseSchema: GenMessage<GetItemsResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 4);
/**
* @generated from message item.v1.CreateItemRequest
*/
export type CreateItemRequest = Message<"item.v1.CreateItemRequest"> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.CreateItemRequest.
* Use `create(CreateItemRequestSchema)` to create a new message.
*/
export const CreateItemRequestSchema: GenMessage<CreateItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 5);
/**
* @generated from message item.v1.CreateItemResponse
*/
export type CreateItemResponse = Message<"item.v1.CreateItemResponse"> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.CreateItemResponse.
* Use `create(CreateItemResponseSchema)` to create a new message.
*/
export const CreateItemResponseSchema: GenMessage<CreateItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 6);
/**
* @generated from message item.v1.UpdateItemRequest
*/
export type UpdateItemRequest = Message<"item.v1.UpdateItemRequest"> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.UpdateItemRequest.
* Use `create(UpdateItemRequestSchema)` to create a new message.
*/
export const UpdateItemRequestSchema: GenMessage<UpdateItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 7);
/**
* @generated from message item.v1.UpdateItemResponse
*/
export type UpdateItemResponse = Message<"item.v1.UpdateItemResponse"> & {
/**
* @generated from field: item.v1.Item item = 1;
*/
item?: Item;
};
/**
* Describes the message item.v1.UpdateItemResponse.
* Use `create(UpdateItemResponseSchema)` to create a new message.
*/
export const UpdateItemResponseSchema: GenMessage<UpdateItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 8);
/**
* @generated from message item.v1.DeleteItemRequest
*/
export type DeleteItemRequest = Message<"item.v1.DeleteItemRequest"> & {
/**
* @generated from field: uint32 id = 1;
*/
id: number;
};
/**
* Describes the message item.v1.DeleteItemRequest.
* Use `create(DeleteItemRequestSchema)` to create a new message.
*/
export const DeleteItemRequestSchema: GenMessage<DeleteItemRequest> = /*@__PURE__*/
messageDesc(file_item_v1_item, 9);
/**
* @generated from message item.v1.DeleteItemResponse
*/
export type DeleteItemResponse = Message<"item.v1.DeleteItemResponse"> & {
};
/**
* Describes the message item.v1.DeleteItemResponse.
* Use `create(DeleteItemResponseSchema)` to create a new message.
*/
export const DeleteItemResponseSchema: GenMessage<DeleteItemResponse> = /*@__PURE__*/
messageDesc(file_item_v1_item, 10);
/**
* @generated from service item.v1.ItemService
*/
export const ItemService: GenService<{
/**
* @generated from rpc item.v1.ItemService.GetItem
*/
getItem: {
methodKind: "unary";
input: typeof GetItemRequestSchema;
output: typeof GetItemResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.GetItems
*/
getItems: {
methodKind: "unary";
input: typeof GetItemsRequestSchema;
output: typeof GetItemsResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.CreateItem
*/
createItem: {
methodKind: "unary";
input: typeof CreateItemRequestSchema;
output: typeof CreateItemResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.UpdateItem
*/
updateItem: {
methodKind: "unary";
input: typeof UpdateItemRequestSchema;
output: typeof UpdateItemResponseSchema;
},
/**
* @generated from rpc item.v1.ItemService.DeleteItem
*/
deleteItem: {
methodKind: "unary";
input: typeof DeleteItemRequestSchema;
output: typeof DeleteItemResponseSchema;
},
}> = /*@__PURE__*/
serviceDesc(file_item_v1_item, 0);

View File

@ -1,8 +1,8 @@
import { createConnectTransport } from "@connectrpc/connect-web"
import { Code, ConnectError, createClient } from "@connectrpc/connect"
import { Code, ConnectError, createClient, type Interceptor } from "@connectrpc/connect"
import { AuthService } from "$lib/services/user/v1/auth_pb";
import { UserService } from "$lib/services/user/v1/user_pb";
import type { Interceptor } from "@connectrpc/connect";
import { ItemService } from "$lib/services/item/v1/item_pb";
import { goto } from "$app/navigation";
const redirector: Interceptor = (next) => async (req) => {
@ -23,4 +23,5 @@ const transport = createConnectTransport({
});
export const AuthClient = createClient(AuthService, transport);
export const UserClient = createClient(UserService, transport);
export const UserClient = createClient(UserService, transport);
export const ItemClient = createClient(ItemService, transport);

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { Dialog } from 'bits-ui';
import { fade } from 'svelte/transition';
import type { Snippet } from 'svelte';
let {
trigger,
content,
open = $bindable(false)
}: { trigger: Snippet; content: Snippet; open: boolean } = $props();
</script>
<Dialog.Root bind:open>
<Dialog.Trigger>
{@render trigger()}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay forceMount>
{#snippet child({ props, open })}
{#if open}
<div
{...props}
transition:fade={{
duration: 100
}}
>
<div class="fixed inset-0 z-50 mt-[50px] bg-black/50 transition-all"></div>
</div>
{/if}
{/snippet}
</Dialog.Overlay>
<Dialog.Content forceMount>
{#snippet child({ props, open })}
{#if open}
<div
{...props}
transition:fade={{
duration: 100
}}
>
<div
class="bg-mantle border-surface-0 fixed inset-0 left-[50%] top-[50%] z-50 size-fit w-96 -translate-x-1/2 -translate-y-1/2 transform overflow-y-auto rounded-xl border pb-1 drop-shadow-md"
>
{@render content()}
</div>
</div>
{/if}
{/snippet}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>

View File

@ -34,7 +34,7 @@
},
{
name: 'Items',
href: '/items',
href: '/items/',
icon: LayoutList
},
{

View File

@ -0,0 +1,332 @@
<script lang="ts">
import { ItemClient } from '$lib/transport';
import { Plus, Trash, Pencil } from '@lucide/svelte';
import { timestampFromDate, timestampDate } from '@bufbuild/protobuf/wkt';
import { Dialog, Button } from 'bits-ui';
import { fade } from 'svelte/transition';
import { toast } from 'svelte-sonner';
import { ConnectError } from '@connectrpc/connect';
import Modal from '$lib/ui/Modal.svelte';
import { SvelteMap } from 'svelte/reactivity';
// Config
let limit: number = $state(10);
let offset: number = $state(0);
let start = $state(new Date(new Date().setDate(new Date().getDate() - 1)));
let end = $state(new Date());
let filter = $state('');
// Items
let items = $state(getItems());
let count: number = $state(0);
// Open
let addedOpen = $state(false);
let deletesOpen: SvelteMap<number, boolean> = new SvelteMap();
let editsOpen: SvelteMap<number, boolean> = new SvelteMap();
async function getItems() {
return await ItemClient.getItems({
limit: limit,
offset: offset,
start: timestampFromDate(start),
end: timestampFromDate(end),
filter: filter
}).then((resp) => {
count = Number(resp.count);
return resp.items;
});
}
async function updateItems() {
let i = getItems();
i.then(() => {
items = i;
});
}
</script>
<div
class="border-surface-0 bg-mantle mx-4 mt-2 overflow-x-auto rounded border-x border-t drop-shadow-md"
>
<table class="w-full table-auto border-collapse text-left rtl:text-right">
<thead>
<tr class="border-surface-0 border-b">
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Added</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Name</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Description</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Price</th>
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Quantity</th>
<th class="w-0"></th>
</tr>
</thead>
<tbody>
{#await items}
<tr class="border-surface-0 border-b">
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
</tr>
{:then items}
{#each items as item}
<tr class="border-surface-0 border-b">
<td class="px-6 py-3">
{item.added ? timestampDate(item.added).toLocaleString() : ''}
</td>
<td class="px-6 py-3">{item.name}</td>
<td class="px-6 py-3">{item.description}</td>
<td class="px-6 py-3">{item.price}</td>
<td class="px-6 py-3">{item.quantity}</td>
<td class="pr-2">
<div class="flex gap-2">
<Modal bind:open={
() => editsOpen.has(item.id!) ? editsOpen.get(item.id!)! : editsOpen.set(item.id!, false) && editsOpen.get(item.id!)!,
(value) => editsOpen.set(item.id!, value)
}>
{#snippet trigger()}
<button
class="bg-text text-crust hover:brightness-120 block cursor-pointer rounded p-2 drop-shadow-md"
>
<Pencil />
</button>
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
Edit {item.name}
</h1>
<form
onsubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const name = formData.get('name')?.toString();
const description = formData.get('description')?.toString();
const price = formData.get('price')?.toString();
const quantity = formData.get('quantity')?.toString();
try {
const response = await ItemClient.updateItem({
item: {
id: item.id,
name: name,
description: description,
price: parseFloat(price ?? '0'),
quantity: parseInt(quantity ?? '0')
}
});
if (response.item && item.id) {
toast.success(`item "${name}" saved`);
editsOpen.set(item.id, false)
await updateItems();
}
} catch (err) {
const error = ConnectError.from(err);
toast.error(error.rawMessage);
}
}}
>
<div class="flex flex-col gap-4 p-3">
<div class="flex flex-col gap-1">
<label for="name" class="text-sm">Name</label>
<input
id="name"
name="name"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
value={item.name}
/>
</div>
<div class="flex flex-col gap-1">
<label for="description" class="text-sm">Description</label>
<input
id="description"
name="description"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
value={item.description}
/>
</div>
<div class="flex flex-col gap-1">
<label for="price" class="text-sm">Price</label>
<input
id="price"
name="price"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
value={item.price}
/>
</div>
<div class="flex flex-col gap-1">
<label for="quantity" class="text-sm">Quantity</label>
<input
id="quantity"
name="quantity"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
value={item.quantity}
/>
</div>
<Button.Root
type="submit"
class="bg-sky text-crust hover:brightness-120 w-20 cursor-pointer rounded p-2 px-4 text-sm transition-all"
>
Submit
</Button.Root>
</div>
</form>
{/snippet}
</Modal>
<Modal bind:open={
() => deletesOpen.has(item.id!) ? deletesOpen.get(item.id!)! : deletesOpen.set(item.id!, false) && deletesOpen.get(item.id!)!,
(value) => deletesOpen.set(item.id!, value)
}>
{#snippet trigger()}
<button
class="bg-red text-crust hover:brightness-120 block cursor-pointer rounded p-2 drop-shadow-md"
>
<Trash />
</button>
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
Delete {item.name}
</h1>
<form
onsubmit={async (e) => {
e.preventDefault();
try {
await ItemClient.deleteItem({
id: item.id
});
toast.success(`item "${item.name}" deleted`);
deletesOpen.set(item.id!, false);
await updateItems();
} catch (err) {
const error = ConnectError.from(err);
toast.error(error.rawMessage);
}
}}
>
<div class="flex flex-col gap-4 p-3">
<span class="text-center">Are you sure you want to delete "{item.name}"?</span
>
<div class="flex justify-center gap-4">
<Button.Root
type="submit"
class="bg-sky text-crust hover:brightness-120 cursor-pointer rounded p-2 px-4 text-sm transition-all"
>
Confirm
</Button.Root>
</div>
</div>
</form>
{/snippet}
</Modal>
</div>
</td>
</tr>
{/each}
{/await}
</tbody>
</table>
</div>
<div class="mx-4 mt-1 flex justify-end">
<Modal bind:open={addedOpen}>
{#snippet trigger()}
<button
class="bg-sky text-crust hover:brightness-120 cursor-pointer rounded p-2 px-4 drop-shadow-md"
>
<Plus />
</button>
{/snippet}
{#snippet content()}
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">Add Item</h1>
<form
onsubmit={async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const name = formData.get('name')?.toString();
const description = formData.get('description')?.toString();
const price = formData.get('price')?.toString();
const quantity = formData.get('quantity')?.toString();
try {
const response = await ItemClient.createItem({
item: {
name: name,
description: description,
price: parseFloat(price ?? '0'),
quantity: parseInt(quantity ?? '0')
}
});
if (response.item) {
form.reset();
toast.success(`item "${name}" added`);
addedOpen = false;
await updateItems();
}
} catch (err) {
const error = ConnectError.from(err);
toast.error(error.rawMessage);
}
}}
>
<div class="flex flex-col gap-4 p-3">
<div class="flex flex-col gap-1">
<label for="name" class="text-sm">Name</label>
<input
id="name"
name="name"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<div class="flex flex-col gap-1">
<label for="description" class="text-sm">Description</label>
<input
id="description"
name="description"
type="text"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<div class="flex flex-col gap-1">
<label for="price" class="text-sm">Price</label>
<input
id="price"
name="price"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<div class="flex flex-col gap-1">
<label for="quantity" class="text-sm">Quantity</label>
<input
id="quantity"
name="quantity"
type="number"
class="border-surface-0 rounded border p-2 text-sm"
/>
</div>
<Button.Root
type="submit"
class="bg-sky text-crust hover:brightness-120 w-fit cursor-pointer rounded p-2 px-4 text-sm transition-all"
>
Submit
</Button.Root>
</div>
</form>
{/snippet}
</Modal>
</div>

View File

@ -15,50 +15,225 @@ components:
scheme: bearer
bearerFormat: JWT
schemas:
user.v1.LoginRequest:
google.protobuf.Timestamp:
type: string
format: date-time
description: |-
A Timestamp represents a point in time independent of any time zone or local
calendar, encoded as a count of seconds and fractions of seconds at
nanosecond resolution. The count is relative to an epoch at UTC midnight on
January 1, 1970, in the proleptic Gregorian calendar which extends the
Gregorian calendar backwards to year one.
All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
second table is needed for interpretation, using a [24-hour linear
smear](https://developers.google.com/time/smear).
The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
restricting to that range, we ensure that we can convert to and from [RFC
3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
# Examples
Example 1: Compute Timestamp from POSIX `time()`.
Timestamp timestamp;
timestamp.set_seconds(time(NULL));
timestamp.set_nanos(0);
Example 2: Compute Timestamp from POSIX `gettimeofday()`.
struct timeval tv;
gettimeofday(&tv, NULL);
Timestamp timestamp;
timestamp.set_seconds(tv.tv_sec);
timestamp.set_nanos(tv.tv_usec * 1000);
Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
// A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
// is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
Timestamp timestamp;
timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
long millis = System.currentTimeMillis();
Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
.setNanos((int) ((millis % 1000) * 1000000)).build();
Example 5: Compute Timestamp from Java `Instant.now()`.
Instant now = Instant.now();
Timestamp timestamp =
Timestamp.newBuilder().setSeconds(now.getEpochSecond())
.setNanos(now.getNano()).build();
Example 6: Compute Timestamp from current time in Python.
timestamp = Timestamp()
timestamp.GetCurrentTime()
# JSON Mapping
In JSON format, the Timestamp type is encoded as a string in the
[RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
where {year} is always expressed using four digits while {month}, {day},
{hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
is required. A proto3 JSON serializer should always use UTC (as indicated by
"Z") when printing the Timestamp type and a proto3 JSON parser should be
able to accept both UTC and other timezones (as indicated by an offset).
For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
01:30 UTC on January 15, 2017.
In JavaScript, one can convert a Date object to this format using the
standard
[toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
method. In Python, a standard `datetime.datetime` object can be converted
to this format using
[`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
the Joda Time's [`ISODateTimeFormat.dateTime()`](
http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()
) to obtain a formatter capable of generating timestamps in this format.
item.v1.CreateItemRequest:
type: object
properties:
username:
type: string
title: username
password:
type: string
title: password
title: LoginRequest
item:
title: item
$ref: '#/components/schemas/item.v1.Item'
title: CreateItemRequest
additionalProperties: false
user.v1.LoginResponse:
item.v1.CreateItemResponse:
type: object
properties:
token:
type: string
title: token
title: LoginResponse
item:
title: item
$ref: '#/components/schemas/item.v1.Item'
title: CreateItemResponse
additionalProperties: false
user.v1.LogoutRequest:
type: object
title: LogoutRequest
additionalProperties: false
user.v1.LogoutResponse:
type: object
title: LogoutResponse
additionalProperties: false
user.v1.SignUpRequest:
item.v1.DeleteItemRequest:
type: object
properties:
username:
type: string
title: username
password:
type: string
title: password
confirmPassword:
type: string
title: confirm_password
title: SignUpRequest
id:
type: integer
title: id
title: DeleteItemRequest
additionalProperties: false
user.v1.SignUpResponse:
item.v1.DeleteItemResponse:
type: object
title: SignUpResponse
title: DeleteItemResponse
additionalProperties: false
item.v1.GetItemRequest:
type: object
properties:
id:
type: integer
title: id
title: GetItemRequest
additionalProperties: false
item.v1.GetItemResponse:
type: object
properties:
item:
title: item
$ref: '#/components/schemas/item.v1.Item'
title: GetItemResponse
additionalProperties: false
item.v1.GetItemsRequest:
type: object
properties:
start:
title: start
nullable: true
$ref: '#/components/schemas/google.protobuf.Timestamp'
end:
title: end
nullable: true
$ref: '#/components/schemas/google.protobuf.Timestamp'
filter:
type: string
title: filter
nullable: true
limit:
type: integer
title: limit
nullable: true
offset:
type: integer
title: offset
nullable: true
title: GetItemsRequest
additionalProperties: false
item.v1.GetItemsResponse:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/item.v1.Item'
title: items
count:
type:
- integer
- string
title: count
format: int64
title: GetItemsResponse
additionalProperties: false
item.v1.Item:
type: object
properties:
id:
type: integer
title: id
nullable: true
name:
type: string
title: name
description:
type: string
title: description
price:
type: number
title: price
format: float
quantity:
type: integer
title: quantity
added:
title: added
nullable: true
$ref: '#/components/schemas/google.protobuf.Timestamp'
title: Item
additionalProperties: false
item.v1.UpdateItemRequest:
type: object
properties:
item:
title: item
$ref: '#/components/schemas/item.v1.Item'
title: UpdateItemRequest
additionalProperties: false
item.v1.UpdateItemResponse:
type: object
properties:
item:
title: item
$ref: '#/components/schemas/item.v1.Item'
title: UpdateItemResponse
additionalProperties: false
connect-protocol-version:
type: number
@ -117,6 +292,51 @@ components:
additionalProperties: true
additionalProperties: true
description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message.
user.v1.LoginRequest:
type: object
properties:
username:
type: string
title: username
password:
type: string
title: password
title: LoginRequest
additionalProperties: false
user.v1.LoginResponse:
type: object
properties:
token:
type: string
title: token
title: LoginResponse
additionalProperties: false
user.v1.LogoutRequest:
type: object
title: LogoutRequest
additionalProperties: false
user.v1.LogoutResponse:
type: object
title: LogoutResponse
additionalProperties: false
user.v1.SignUpRequest:
type: object
properties:
username:
type: string
title: username
password:
type: string
title: password
confirmPassword:
type: string
title: confirm_password
title: SignUpRequest
additionalProperties: false
user.v1.SignUpResponse:
type: object
title: SignUpResponse
additionalProperties: false
user.v1.APIKeyRequest:
type: object
properties:
@ -157,6 +377,181 @@ components:
security:
- bearerAuth: []
paths:
/item.v1.ItemService/GetItem:
post:
tags:
- item.v1.ItemService
summary: GetItem
operationId: item.v1.ItemService.GetItem
parameters:
- name: Connect-Protocol-Version
in: header
required: true
schema:
$ref: '#/components/schemas/connect-protocol-version'
- name: Connect-Timeout-Ms
in: header
schema:
$ref: '#/components/schemas/connect-timeout-header'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.GetItemRequest'
required: true
responses:
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/connect.error'
"200":
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.GetItemResponse'
/item.v1.ItemService/GetItems:
post:
tags:
- item.v1.ItemService
summary: GetItems
operationId: item.v1.ItemService.GetItems
parameters:
- name: Connect-Protocol-Version
in: header
required: true
schema:
$ref: '#/components/schemas/connect-protocol-version'
- name: Connect-Timeout-Ms
in: header
schema:
$ref: '#/components/schemas/connect-timeout-header'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.GetItemsRequest'
required: true
responses:
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/connect.error'
"200":
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.GetItemsResponse'
/item.v1.ItemService/CreateItem:
post:
tags:
- item.v1.ItemService
summary: CreateItem
operationId: item.v1.ItemService.CreateItem
parameters:
- name: Connect-Protocol-Version
in: header
required: true
schema:
$ref: '#/components/schemas/connect-protocol-version'
- name: Connect-Timeout-Ms
in: header
schema:
$ref: '#/components/schemas/connect-timeout-header'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.CreateItemRequest'
required: true
responses:
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/connect.error'
"200":
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.CreateItemResponse'
/item.v1.ItemService/UpdateItem:
post:
tags:
- item.v1.ItemService
summary: UpdateItem
operationId: item.v1.ItemService.UpdateItem
parameters:
- name: Connect-Protocol-Version
in: header
required: true
schema:
$ref: '#/components/schemas/connect-protocol-version'
- name: Connect-Timeout-Ms
in: header
schema:
$ref: '#/components/schemas/connect-timeout-header'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.UpdateItemRequest'
required: true
responses:
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/connect.error'
"200":
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.UpdateItemResponse'
/item.v1.ItemService/DeleteItem:
post:
tags:
- item.v1.ItemService
summary: DeleteItem
operationId: item.v1.ItemService.DeleteItem
parameters:
- name: Connect-Protocol-Version
in: header
required: true
schema:
$ref: '#/components/schemas/connect-protocol-version'
- name: Connect-Timeout-Ms
in: header
schema:
$ref: '#/components/schemas/connect-timeout-header'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.DeleteItemRequest'
required: true
responses:
default:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/connect.error'
"200":
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/item.v1.DeleteItemResponse'
/user.v1.AuthService/Login:
post:
tags:
@ -333,5 +728,6 @@ paths:
schema:
$ref: '#/components/schemas/user.v1.APIKeyResponse'
tags:
- name: item.v1.ItemService
- name: user.v1.AuthService
- name: user.v1.UserService