@robbeverhelst/do 1.392.0 → 1.394.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.
Files changed (2) hide show
  1. package/dist/do.js +644 -185
  2. package/package.json +1 -1
package/dist/do.js CHANGED
@@ -14420,7 +14420,7 @@ var configError = (message) => new CliError("config", message, EXIT.CONFIG);
14420
14420
 
14421
14421
  // src/meta.ts
14422
14422
  var CLI_NAME = "do";
14423
- var VERSION = "1.392.0";
14423
+ var VERSION = "1.394.0";
14424
14424
  var API_PREFIX = "/api/v1";
14425
14425
  var DEFAULT_API_URL = "http://localhost:3001";
14426
14426
 
@@ -20637,6 +20637,25 @@ var files = pgTable("files", {
20637
20637
  index("files_owner_idx").on(t.ownerNodeId, t.createdAt)
20638
20638
  ]);
20639
20639
 
20640
+ // ../../packages/schema/src/db/inventory.ts
20641
+ var inventoryItems = pgTable("inventory_items", {
20642
+ ...leafColumns(),
20643
+ productId: text("product_id").references(() => products.id),
20644
+ quantity: doublePrecision("quantity").notNull().default(0),
20645
+ unit: text("unit"),
20646
+ category: text("category"),
20647
+ location: text("location"),
20648
+ lowStockThreshold: doublePrecision("low_stock_threshold"),
20649
+ expiresAt: timestamp("expires_at", { withTimezone: true }),
20650
+ notes: text("notes"),
20651
+ facet: jsonb("facet").$type(),
20652
+ placeId: text("place_id").references(() => places.id, { onDelete: "set null" })
20653
+ }, (t) => [
20654
+ index("inventory_items_tenant_place_idx").on(t.tenantId, t.placeId),
20655
+ index("inventory_items_tenant_idx").on(t.tenantId),
20656
+ index("inventory_items_product_idx").on(t.productId)
20657
+ ]);
20658
+
20640
20659
  // ../../packages/schema/src/db/periods.ts
20641
20660
  var periodKind = pgEnum("period_kind", [
20642
20661
  "hibernation",
@@ -20801,6 +20820,64 @@ var animalHandoverNotes = pgTable("animal_handover_notes", {
20801
20820
  photoFileId: text("photo_file_id").references(() => files.id, { onDelete: "set null" }),
20802
20821
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
20803
20822
  }, (t) => [index("animal_handover_notes_period_idx").on(t.tenantId, t.periodId)]);
20823
+ var animalFeedings = pgTable("animal_feedings", {
20824
+ id: text("id").primaryKey(),
20825
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
20826
+ animalId: text("animal_id").notNull().references(() => animals.id, { onDelete: "cascade" }),
20827
+ supplyId: text("supply_id").notNull().references(() => inventoryItems.id, { onDelete: "cascade" }),
20828
+ what: text("what"),
20829
+ amount: text("amount"),
20830
+ times: text("times"),
20831
+ note: text("note"),
20832
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
20833
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
20834
+ }, (t) => [
20835
+ unique("animal_feedings_animal_supply_uniq").on(t.tenantId, t.animalId, t.supplyId),
20836
+ index("animal_feedings_supply_idx").on(t.tenantId, t.supplyId)
20837
+ ]);
20838
+ var animalMedicineEveryUnit = pgEnum("animal_medicine_every_unit", [
20839
+ "day",
20840
+ "week",
20841
+ "month"
20842
+ ]);
20843
+ var animalMedicines = pgTable("animal_medicines", {
20844
+ id: text("id").primaryKey(),
20845
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
20846
+ title: text("title").notNull(),
20847
+ dose: text("dose"),
20848
+ everyCount: integer2("every_count").notNull().default(1),
20849
+ everyUnit: animalMedicineEveryUnit("every_unit").notNull().default("day"),
20850
+ timeOfDay: text("time_of_day"),
20851
+ startsOn: date5("starts_on", { mode: "string" }),
20852
+ endsOn: date5("ends_on", { mode: "string" }),
20853
+ nextOn: date5("next_on", { mode: "string" }),
20854
+ stock: doublePrecision("stock"),
20855
+ unit: text("unit"),
20856
+ perDose: doublePrecision("per_dose").notNull().default(1),
20857
+ lowAt: doublePrecision("low_at"),
20858
+ note: text("note"),
20859
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
20860
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
20861
+ }, (t) => [index("animal_medicines_tenant_idx").on(t.tenantId)]);
20862
+ var animalMedicineAnimals = pgTable("animal_medicine_animals", {
20863
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
20864
+ medicineId: text("medicine_id").notNull().references(() => animalMedicines.id, { onDelete: "cascade" }),
20865
+ animalId: text("animal_id").notNull().references(() => animals.id, { onDelete: "cascade" })
20866
+ }, (t) => [
20867
+ primaryKey({ columns: [t.medicineId, t.animalId] }),
20868
+ index("animal_medicine_animals_animal_idx").on(t.tenantId, t.animalId)
20869
+ ]);
20870
+ var animalMedicineDoses = pgTable("animal_medicine_doses", {
20871
+ id: text("id").primaryKey(),
20872
+ tenantId: text("tenant_id").notNull().references(() => tenants.id),
20873
+ medicineId: text("medicine_id").notNull().references(() => animalMedicines.id, { onDelete: "cascade" }),
20874
+ day: date5("day", { mode: "string" }).notNull(),
20875
+ givenAt: timestamp("given_at", { withTimezone: true }).notNull().defaultNow(),
20876
+ givenByUserId: text("given_by_user_id").references(() => users.id, { onDelete: "set null" }),
20877
+ took: doublePrecision("took").notNull().default(0),
20878
+ nextWas: date5("next_was", { mode: "string" }),
20879
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
20880
+ }, (t) => [unique("animal_medicine_doses_day_uniq").on(t.medicineId, t.day)]);
20804
20881
 
20805
20882
  // ../../packages/schema/src/db/documents.ts
20806
20883
  var documentKind = pgEnum("document_kind", [
@@ -21210,6 +21287,21 @@ var animalHabitSchema = nodeRefSchema.extend({
21210
21287
  routine: string2().nullable().optional(),
21211
21288
  animalIds: array(ulidSchema).default([])
21212
21289
  });
21290
+ var animalFeedingSchema = object({
21291
+ animalId: ulidSchema,
21292
+ supplyId: ulidSchema,
21293
+ what: string2().nullable(),
21294
+ amount: string2().nullable(),
21295
+ times: string2().nullable(),
21296
+ note: string2().nullable()
21297
+ });
21298
+ var feedingText = (max) => string2().trim().min(1).max(max).nullish();
21299
+ var animalFeedingInputSchema = object({
21300
+ what: feedingText(120),
21301
+ amount: feedingText(60),
21302
+ times: feedingText(80),
21303
+ note: feedingText(200)
21304
+ }).strict();
21213
21305
  var animalSupplySchema = nodeRefSchema.extend({
21214
21306
  quantity: number2(),
21215
21307
  unit: string2().nullable(),
@@ -21219,7 +21311,8 @@ var animalSupplySchema = nodeRefSchema.extend({
21219
21311
  daysLeft: number2().int().nullable(),
21220
21312
  full: number2().nullable(),
21221
21313
  onList: string2().nullable(),
21222
- animalIds: array(ulidSchema).optional()
21314
+ animalIds: array(ulidSchema).optional(),
21315
+ feedings: array(animalFeedingSchema).optional()
21223
21316
  });
21224
21317
  var animalDayCountSchema = object({ day: string2(), count: number2().int() });
21225
21318
  var animalWeightPointSchema = object({
@@ -21712,6 +21805,61 @@ var animalArrivedInputSchema = object({
21712
21805
  on: keyDateDaySchema.optional(),
21713
21806
  quarantineMonths: number2().int().min(0).max(12).optional()
21714
21807
  }).strict();
21808
+ var animalMedicineEveryUnitSchema = _enum2(animalMedicineEveryUnit.enumValues);
21809
+ var animalMedicineViewSchema = object({
21810
+ id: ulidSchema,
21811
+ title: string2(),
21812
+ dose: string2().nullable(),
21813
+ everyCount: number2().int(),
21814
+ everyUnit: animalMedicineEveryUnitSchema,
21815
+ timeOfDay: string2().nullable(),
21816
+ startsOn: string2().nullable(),
21817
+ endsOn: string2().nullable(),
21818
+ nextOn: string2().nullable(),
21819
+ stock: number2().nullable(),
21820
+ unit: string2().nullable(),
21821
+ perDose: number2(),
21822
+ lowAt: number2().nullable(),
21823
+ note: string2().nullable(),
21824
+ animals: array(nodeRefSchema),
21825
+ daily: boolean2(),
21826
+ low: boolean2(),
21827
+ ended: boolean2(),
21828
+ course: object({ day: number2().int(), of: number2().int() }).nullable(),
21829
+ dueToday: boolean2(),
21830
+ doneToday: boolean2(),
21831
+ lastGivenOn: string2().nullable(),
21832
+ createdAt: exports_coerce.date(),
21833
+ updatedAt: exports_coerce.date()
21834
+ });
21835
+ var medicineText = (max) => string2().trim().min(1).max(max);
21836
+ var animalMedicineFields = object({
21837
+ id: ulidSchema,
21838
+ title: medicineText(120),
21839
+ animalIds: array(ulidSchema).min(1).max(20),
21840
+ dose: medicineText(120).nullish(),
21841
+ everyCount: number2().int().min(1).max(366).optional(),
21842
+ everyUnit: animalMedicineEveryUnitSchema.optional(),
21843
+ timeOfDay: string2().regex(/^(morning|midday|afternoon|evening|night|([01][0-9]|2[0-3]):[0-5][0-9])$/).nullish(),
21844
+ startsOn: keyDateDaySchema.nullish(),
21845
+ endsOn: keyDateDaySchema.nullish(),
21846
+ nextOn: keyDateDaySchema.nullish(),
21847
+ stock: number2().min(0).max(1e5).nullish(),
21848
+ unit: medicineText(30).nullish(),
21849
+ perDose: number2().positive().max(1e4).optional(),
21850
+ lowAt: number2().min(0).max(1e5).nullish(),
21851
+ note: medicineText(300).nullish()
21852
+ }).strict();
21853
+ var endsAfterStart2 = (v) => !v.startsOn || !v.endsOn || v.endsOn >= v.startsOn;
21854
+ var endsMessage = { message: "endsOn is before startsOn", path: ["endsOn"] };
21855
+ var createAnimalMedicineInputSchema = animalMedicineFields.refine(endsAfterStart2, endsMessage);
21856
+ var updateAnimalMedicineInputSchema = animalMedicineFields.omit({ id: true }).partial().strict().refine(endsAfterStart2, endsMessage);
21857
+ var animalMedicinesQuerySchema = object({
21858
+ animalId: ulidSchema.optional(),
21859
+ day: keyDateDaySchema.optional(),
21860
+ includeEnded: _enum2(["true", "false"]).transform((v) => v === "true").optional()
21861
+ });
21862
+ var animalMedicineDoseInputSchema = object({ day: keyDateDaySchema.optional() }).strict();
21715
21863
 
21716
21864
  // ../../packages/schema/src/operations/crud.ts
21717
21865
  var DELETE_REASON_QUERY = [
@@ -21795,6 +21943,7 @@ function crudOperations(r) {
21795
21943
  var READ = ["animals:read"];
21796
21944
  var WRITE = ["animals:write"];
21797
21945
  var visit = { visitId: "The vet visit's ULID." };
21946
+ var medicine = { medicineId: "The medicine's ULID." };
21798
21947
  var animalOperations = defineOperations({
21799
21948
  ...crudOperations({
21800
21949
  segment: "animals",
@@ -21883,6 +22032,96 @@ var animalOperations = defineOperations({
21883
22032
  response: { description: "The species profile." },
21884
22033
  errors: { 400: true }
21885
22034
  },
22035
+ listMedicines: {
22036
+ method: "get",
22037
+ path: "/animals/medicines",
22038
+ scopes: READ,
22039
+ tag: "Animals",
22040
+ summary: "List the animals' medicines",
22041
+ description: "By title: each medicine with the `animals` it's for, its `dose`, how often (`everyCount` `everyUnit`; `daily` when every 1 day), `timeOfDay`, a course's `startsOn`/`endsOn` and its `course` day N of M, the `nextOn` day of one that isn't daily, the counted `stock` (`unit`, `perDose`, `low` at `lowAt`), the `note`, and on `day` (UTC today by default) whether it's `dueToday` and `doneToday`, with `lastGivenOn`. Ended courses only with `includeEnded`.",
22042
+ query: [
22043
+ { name: "animalId", description: "Only this animal's (an animal's ULID)." },
22044
+ { name: "day", description: "The reader's day, `YYYY-MM-DD`; UTC today when left out." },
22045
+ { name: "includeEnded", description: "`true`: ended courses too." }
22046
+ ],
22047
+ querySchema: animalMedicinesQuerySchema,
22048
+ status: 200,
22049
+ response: { description: "The medicines." }
22050
+ },
22051
+ createMedicine: {
22052
+ method: "post",
22053
+ path: "/animals/medicines",
22054
+ scopes: WRITE,
22055
+ tag: "Animals",
22056
+ summary: "Add a medicine for one animal or a few",
22057
+ description: "A daily one (the default, every 1 day), a course (`startsOn` to `endsOn`) or one that comes round (`everyCount` `everyUnit`, with its `nextOn`). Count the `stock` to have each dose take `perDose` (default 1) from it and say low at `lowAt`. Every animal must be one in this Space (400).",
22058
+ body: createAnimalMedicineInputSchema,
22059
+ status: 201,
22060
+ response: { description: "The medicine." },
22061
+ errors: { 400: true }
22062
+ },
22063
+ getMedicine: {
22064
+ method: "get",
22065
+ path: "/animals/medicines/{medicineId}",
22066
+ pathParams: medicine,
22067
+ scopes: READ,
22068
+ tag: "Animals",
22069
+ summary: "A medicine, as it stands today",
22070
+ description: "The same shape as a row of the list, on UTC today.",
22071
+ status: 200,
22072
+ response: { description: "The medicine." },
22073
+ errors: { 404: true }
22074
+ },
22075
+ updateMedicine: {
22076
+ method: "patch",
22077
+ path: "/animals/medicines/{medicineId}",
22078
+ pathParams: medicine,
22079
+ scopes: WRITE,
22080
+ tag: "Animals",
22081
+ summary: "Change a medicine",
22082
+ description: "A field left out stays; `null` clears it; `animalIds` replaces who it's for. Set `stock` when a new box comes in.",
22083
+ body: updateAnimalMedicineInputSchema,
22084
+ status: 200,
22085
+ response: { description: "The medicine." },
22086
+ errors: { 400: true, 404: true }
22087
+ },
22088
+ removeMedicine: {
22089
+ method: "delete",
22090
+ path: "/animals/medicines/{medicineId}",
22091
+ pathParams: medicine,
22092
+ scopes: WRITE,
22093
+ tag: "Animals",
22094
+ summary: "Remove a medicine",
22095
+ description: "With the doses given; to stop a course and keep it, set its `endsOn` instead.",
22096
+ status: 204,
22097
+ response: { description: "Removed." },
22098
+ errors: { 404: true }
22099
+ },
22100
+ giveMedicineDose: {
22101
+ method: "post",
22102
+ path: "/animals/medicines/{medicineId}/doses",
22103
+ pathParams: medicine,
22104
+ scopes: WRITE,
22105
+ tag: "Animals",
22106
+ summary: "Give a medicine's dose",
22107
+ description: "The dose for `day` (UTC today by default): it takes `perDose` from a counted stock (never below 0) and moves the next day of one that isn't daily on from that day. Again that day changes nothing.",
22108
+ body: animalMedicineDoseInputSchema,
22109
+ status: 200,
22110
+ response: { description: "The medicine, on that day." },
22111
+ errors: { 400: true, 404: true }
22112
+ },
22113
+ undoMedicineDose: {
22114
+ method: "delete",
22115
+ path: "/animals/medicines/{medicineId}/doses/{day}",
22116
+ pathParams: { ...medicine, day: "The day it was given for, `YYYY-MM-DD`." },
22117
+ scopes: WRITE,
22118
+ tag: "Animals",
22119
+ summary: "Take a medicine's dose back",
22120
+ description: "The stock and the next day go back to before it; 404 when none was given that day.",
22121
+ status: 200,
22122
+ response: { description: "The medicine, on that day." },
22123
+ errors: { 400: "`day` isn't a `YYYY-MM-DD` day.", 404: true }
22124
+ },
21886
22125
  vetVisit: {
21887
22126
  method: "get",
21888
22127
  path: "/animals/vet-visits/{visitId}",
@@ -21935,6 +22174,29 @@ var animalOperations = defineOperations({
21935
22174
  response: { description: "The timeline entries." },
21936
22175
  errors: { 404: true }
21937
22176
  },
22177
+ setFeeding: {
22178
+ method: "put",
22179
+ path: "/animals/{id}/feeding/{supplyId}",
22180
+ scopes: WRITE,
22181
+ tag: "Animals",
22182
+ summary: "Say how an animal is fed from a supply",
22183
+ description: "What it gets (`what`, when it's more than the supply's name), how much (`amount`, \"2 \xD7 110 g\"), when (`times`, \"07:30 and 18:00\") and a line for whoever feeds it (`note`), each as written. A field left out stays; `null` clears it. Links the supply (a Household item) to the animal when it doesn't feed it yet. Read back on the animal's and the overview's supplies, as `feedings`.",
22184
+ body: animalFeedingInputSchema,
22185
+ status: 200,
22186
+ response: { description: "The feeding." },
22187
+ errors: { 400: true, 404: true }
22188
+ },
22189
+ removeFeeding: {
22190
+ method: "delete",
22191
+ path: "/animals/{id}/feeding/{supplyId}",
22192
+ scopes: WRITE,
22193
+ tag: "Animals",
22194
+ summary: "Take an animal's feeding off a supply",
22195
+ description: "The supply still feeds it; only what was said about how is gone.",
22196
+ status: 204,
22197
+ response: { description: "Gone." },
22198
+ errors: { 404: true }
22199
+ },
21938
22200
  lostCard: {
21939
22201
  method: "get",
21940
22202
  path: "/animals/{id}/lost-card",
@@ -23985,6 +24247,163 @@ var collectionEntryKindValues = [
23985
24247
  ];
23986
24248
  var collectionEntryKindSchema = _enum2(collectionEntryKindValues);
23987
24249
 
24250
+ // ../../packages/schema/src/contracts/collection-kinds.ts
24251
+ var collectionKindFieldTypeValues = [
24252
+ "text",
24253
+ "number",
24254
+ "choice",
24255
+ "year",
24256
+ "date",
24257
+ "yesno"
24258
+ ];
24259
+ var collectionKindFieldTypeSchema = _enum2(collectionKindFieldTypeValues);
24260
+ var collectionKindIconValues = [
24261
+ "disc",
24262
+ "dice",
24263
+ "brick",
24264
+ "book",
24265
+ "pad",
24266
+ "sprout",
24267
+ "paw",
24268
+ "tag"
24269
+ ];
24270
+ var collectionKindIconSchema = _enum2(collectionKindIconValues);
24271
+ var collectionKindPresetValues = ["lego", "vinyl", "board", "custom"];
24272
+ var collectionKindPresetSchema = _enum2(collectionKindPresetValues);
24273
+ var slug = string2().min(1).max(40).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "lowercase letters and digits, dashes between");
24274
+ var collectionKindKeySchema = slug.refine((k) => !collectionTypeValues.includes(k), "that's a built-in kind");
24275
+ var collectionKindFieldSchema = object({
24276
+ key: slug,
24277
+ label: string2().trim().min(1).max(40),
24278
+ type: collectionKindFieldTypeSchema,
24279
+ choices: array(string2().trim().min(1).max(40)).max(30).optional()
24280
+ }).strict().refine((f) => f.type === "choice" || f.choices === undefined, {
24281
+ message: "only a choice field has choices",
24282
+ path: ["choices"]
24283
+ });
24284
+ var collectionKindFieldsSchema = array(collectionKindFieldSchema).max(12).refine((fs) => new Set(fs.map((f) => f.key)).size === fs.length, "each field key once");
24285
+ var word = string2().trim().min(1).max(30);
24286
+ var collectionKindStatusWordsSchema = object({ want: word, have: word, doing: word, done: word, dropped: word }).strict();
24287
+ var collectionKindSchema = object({
24288
+ id: ulidSchema,
24289
+ key: string2(),
24290
+ name: string2(),
24291
+ one: string2(),
24292
+ many: string2(),
24293
+ icon: string2(),
24294
+ preset: string2().nullable(),
24295
+ fields: array(object({
24296
+ key: string2(),
24297
+ label: string2(),
24298
+ type: string2(),
24299
+ choices: array(string2()).optional()
24300
+ })),
24301
+ statusWords: collectionKindStatusWordsSchema,
24302
+ view: string2(),
24303
+ itemCount: number2().int(),
24304
+ createdAt: exports_coerce.date(),
24305
+ updatedAt: exports_coerce.date()
24306
+ });
24307
+ var name2 = string2().trim().min(1).max(40);
24308
+ var createCollectionKindInputSchema = object({
24309
+ id: ulidSchema,
24310
+ name: name2,
24311
+ key: collectionKindKeySchema.optional(),
24312
+ one: name2.optional(),
24313
+ many: name2.optional(),
24314
+ icon: collectionKindIconSchema.optional(),
24315
+ preset: collectionKindPresetSchema.optional(),
24316
+ fields: collectionKindFieldsSchema.optional(),
24317
+ statusWords: collectionKindStatusWordsSchema.optional()
24318
+ }).strict();
24319
+ var updateCollectionKindInputSchema = object({
24320
+ name: name2.optional(),
24321
+ one: name2.optional(),
24322
+ many: name2.optional(),
24323
+ icon: collectionKindIconSchema.optional(),
24324
+ fields: collectionKindFieldsSchema.optional(),
24325
+ statusWords: collectionKindStatusWordsSchema.optional()
24326
+ }).strict();
24327
+ var f = (label, type) => ({
24328
+ key: kindSlug(label),
24329
+ label,
24330
+ type
24331
+ });
24332
+ var COLLECTION_KIND_PRESETS = {
24333
+ lego: {
24334
+ name: "LEGO",
24335
+ one: "set",
24336
+ icon: "brick",
24337
+ fields: [
24338
+ f("Set number", "text"),
24339
+ f("Pieces", "number"),
24340
+ f("Theme", "choice"),
24341
+ f("Bags", "number"),
24342
+ f("Minifigures", "number"),
24343
+ f("Released", "year")
24344
+ ],
24345
+ statusWords: {
24346
+ want: "Want",
24347
+ have: "In the box",
24348
+ doing: "Building",
24349
+ done: "Built",
24350
+ dropped: "Parted out"
24351
+ }
24352
+ },
24353
+ vinyl: {
24354
+ name: "Vinyl",
24355
+ one: "record",
24356
+ icon: "disc",
24357
+ fields: [
24358
+ f("Artist", "text"),
24359
+ f("Label", "choice"),
24360
+ f("Pressing", "text"),
24361
+ f("Speed", "number"),
24362
+ f("Condition", "choice"),
24363
+ f("Released", "year")
24364
+ ],
24365
+ statusWords: {
24366
+ want: "Want",
24367
+ have: "Have",
24368
+ doing: "On rotation",
24369
+ done: "Played out",
24370
+ dropped: "Sold"
24371
+ }
24372
+ },
24373
+ board: {
24374
+ name: "Board games",
24375
+ one: "board game",
24376
+ icon: "dice",
24377
+ fields: [
24378
+ f("Players", "number"),
24379
+ f("Play time", "text"),
24380
+ f("Designer", "text"),
24381
+ f("Expansions", "text"),
24382
+ f("Plays", "number"),
24383
+ f("Released", "year")
24384
+ ],
24385
+ statusWords: {
24386
+ want: "Want",
24387
+ have: "Have",
24388
+ doing: "Learning",
24389
+ done: "Played",
24390
+ dropped: "Gave away"
24391
+ }
24392
+ },
24393
+ custom: {
24394
+ name: "",
24395
+ one: "",
24396
+ icon: "tag",
24397
+ fields: [f("Maker", "text"), f("Year", "year"), f("Condition", "choice")],
24398
+ statusWords: { want: "Want", have: "Have", doing: "Using", done: "Done", dropped: "Gone" }
24399
+ }
24400
+ };
24401
+ function kindSlug(name) {
24402
+ const s = name.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036F]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
24403
+ return s || "kind";
24404
+ }
24405
+ var kindCoverUrl = string2().url().max(2048);
24406
+
23988
24407
  // ../../packages/schema/src/contracts/collection.ts
23989
24408
  var collectionRatingSchema = number2().min(0.5).max(5).step(0.5, "must be a whole or half star (0.5 steps)");
23990
24409
  var collectionFacetSchema = record(string2(), unknown());
@@ -24008,7 +24427,7 @@ var collectionItemSchema = createSelectSchema(collectionItems, {
24008
24427
  });
24009
24428
  var createCollectionItemInputSchema = createInsertSchema(collectionItems, {
24010
24429
  id: ulidSchema,
24011
- type: collectionTypeSchema,
24430
+ type: union([collectionTypeSchema, collectionKindKeySchema]),
24012
24431
  status: collectionStatusSchema.optional(),
24013
24432
  quantity: number2().min(0).nullish(),
24014
24433
  areaId: ulidSchema.nullish(),
@@ -26157,162 +26576,6 @@ var circleOperations = defineOperations({
26157
26576
  })
26158
26577
  });
26159
26578
 
26160
- // ../../packages/schema/src/contracts/collection-kinds.ts
26161
- var collectionKindFieldTypeValues = [
26162
- "text",
26163
- "number",
26164
- "choice",
26165
- "year",
26166
- "date",
26167
- "yesno"
26168
- ];
26169
- var collectionKindFieldTypeSchema = _enum2(collectionKindFieldTypeValues);
26170
- var collectionKindIconValues = [
26171
- "disc",
26172
- "dice",
26173
- "brick",
26174
- "book",
26175
- "pad",
26176
- "sprout",
26177
- "paw",
26178
- "tag"
26179
- ];
26180
- var collectionKindIconSchema = _enum2(collectionKindIconValues);
26181
- var collectionKindPresetValues = ["lego", "vinyl", "board", "custom"];
26182
- var collectionKindPresetSchema = _enum2(collectionKindPresetValues);
26183
- var slug = string2().min(1).max(40).regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "lowercase letters and digits, dashes between");
26184
- var collectionKindKeySchema = slug.refine((k) => !collectionTypeValues.includes(k), "that's a built-in kind");
26185
- var collectionKindFieldSchema = object({
26186
- key: slug,
26187
- label: string2().trim().min(1).max(40),
26188
- type: collectionKindFieldTypeSchema,
26189
- choices: array(string2().trim().min(1).max(40)).max(30).optional()
26190
- }).strict().refine((f) => f.type === "choice" || f.choices === undefined, {
26191
- message: "only a choice field has choices",
26192
- path: ["choices"]
26193
- });
26194
- var collectionKindFieldsSchema = array(collectionKindFieldSchema).max(12).refine((fs) => new Set(fs.map((f) => f.key)).size === fs.length, "each field key once");
26195
- var word = string2().trim().min(1).max(30);
26196
- var collectionKindStatusWordsSchema = object({ want: word, have: word, doing: word, done: word, dropped: word }).strict();
26197
- var collectionKindSchema = object({
26198
- id: ulidSchema,
26199
- key: string2(),
26200
- name: string2(),
26201
- one: string2(),
26202
- many: string2(),
26203
- icon: string2(),
26204
- preset: string2().nullable(),
26205
- fields: array(object({
26206
- key: string2(),
26207
- label: string2(),
26208
- type: string2(),
26209
- choices: array(string2()).optional()
26210
- })),
26211
- statusWords: collectionKindStatusWordsSchema,
26212
- view: string2(),
26213
- itemCount: number2().int(),
26214
- createdAt: exports_coerce.date(),
26215
- updatedAt: exports_coerce.date()
26216
- });
26217
- var name2 = string2().trim().min(1).max(40);
26218
- var createCollectionKindInputSchema = object({
26219
- id: ulidSchema,
26220
- name: name2,
26221
- key: collectionKindKeySchema.optional(),
26222
- one: name2.optional(),
26223
- many: name2.optional(),
26224
- icon: collectionKindIconSchema.optional(),
26225
- preset: collectionKindPresetSchema.optional(),
26226
- fields: collectionKindFieldsSchema.optional(),
26227
- statusWords: collectionKindStatusWordsSchema.optional()
26228
- }).strict();
26229
- var updateCollectionKindInputSchema = object({
26230
- name: name2.optional(),
26231
- one: name2.optional(),
26232
- many: name2.optional(),
26233
- icon: collectionKindIconSchema.optional(),
26234
- fields: collectionKindFieldsSchema.optional(),
26235
- statusWords: collectionKindStatusWordsSchema.optional()
26236
- }).strict();
26237
- var f = (label, type) => ({
26238
- key: kindSlug(label),
26239
- label,
26240
- type
26241
- });
26242
- var COLLECTION_KIND_PRESETS = {
26243
- lego: {
26244
- name: "LEGO",
26245
- one: "set",
26246
- icon: "brick",
26247
- fields: [
26248
- f("Set number", "text"),
26249
- f("Pieces", "number"),
26250
- f("Theme", "choice"),
26251
- f("Bags", "number"),
26252
- f("Minifigures", "number"),
26253
- f("Released", "year")
26254
- ],
26255
- statusWords: {
26256
- want: "Want",
26257
- have: "In the box",
26258
- doing: "Building",
26259
- done: "Built",
26260
- dropped: "Parted out"
26261
- }
26262
- },
26263
- vinyl: {
26264
- name: "Vinyl",
26265
- one: "record",
26266
- icon: "disc",
26267
- fields: [
26268
- f("Artist", "text"),
26269
- f("Label", "choice"),
26270
- f("Pressing", "text"),
26271
- f("Speed", "number"),
26272
- f("Condition", "choice"),
26273
- f("Released", "year")
26274
- ],
26275
- statusWords: {
26276
- want: "Want",
26277
- have: "Have",
26278
- doing: "On rotation",
26279
- done: "Played out",
26280
- dropped: "Sold"
26281
- }
26282
- },
26283
- board: {
26284
- name: "Board games",
26285
- one: "board game",
26286
- icon: "dice",
26287
- fields: [
26288
- f("Players", "number"),
26289
- f("Play time", "text"),
26290
- f("Designer", "text"),
26291
- f("Expansions", "text"),
26292
- f("Plays", "number"),
26293
- f("Released", "year")
26294
- ],
26295
- statusWords: {
26296
- want: "Want",
26297
- have: "Have",
26298
- doing: "Learning",
26299
- done: "Played",
26300
- dropped: "Gave away"
26301
- }
26302
- },
26303
- custom: {
26304
- name: "",
26305
- one: "",
26306
- icon: "tag",
26307
- fields: [f("Maker", "text"), f("Year", "year"), f("Condition", "choice")],
26308
- statusWords: { want: "Want", have: "Have", doing: "Using", done: "Done", dropped: "Gone" }
26309
- }
26310
- };
26311
- function kindSlug(name) {
26312
- const s = name.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036F]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
26313
- return s || "kind";
26314
- }
26315
-
26316
26579
  // ../../packages/schema/src/contracts/pagination.ts
26317
26580
  var paginationQuerySchema = object({
26318
26581
  limit: exports_coerce.number().int().positive().optional(),
@@ -31543,9 +31806,9 @@ var eventFields = {
31543
31806
  note: eventTextSchema(1000).nullish(),
31544
31807
  sort: number2().int().min(0).max(1e4).optional()
31545
31808
  };
31546
- var endsAfterStart2 = (e) => !e.startsAt || !e.endsAt || e.endsAt >= e.startsAt;
31547
- var createGuestListEventInputSchema = object({ id: ulidSchema, guestListId: ulidSchema, key: guestEventKeySchema, ...eventFields }).strict().refine(endsAfterStart2, { message: "endsAt is before startsAt", path: ["endsAt"] });
31548
- var updateGuestListEventInputSchema = object(eventFields).partial().strict().refine(endsAfterStart2, { message: "endsAt is before startsAt", path: ["endsAt"] });
31809
+ var endsAfterStart3 = (e) => !e.startsAt || !e.endsAt || e.endsAt >= e.startsAt;
31810
+ var createGuestListEventInputSchema = object({ id: ulidSchema, guestListId: ulidSchema, key: guestEventKeySchema, ...eventFields }).strict().refine(endsAfterStart3, { message: "endsAt is before startsAt", path: ["endsAt"] });
31811
+ var updateGuestListEventInputSchema = object(eventFields).partial().strict().refine(endsAfterStart3, { message: "endsAt is before startsAt", path: ["endsAt"] });
31549
31812
  var guestListEventsQuerySchema = object({ guestListId: ulidSchema });
31550
31813
  var guestTableSchema = createSelectSchema(guestTables, {
31551
31814
  createdAt: exports_coerce.date(),
@@ -34179,25 +34442,6 @@ var incidentOperations = defineOperations({
34179
34442
  })
34180
34443
  });
34181
34444
 
34182
- // ../../packages/schema/src/db/inventory.ts
34183
- var inventoryItems = pgTable("inventory_items", {
34184
- ...leafColumns(),
34185
- productId: text("product_id").references(() => products.id),
34186
- quantity: doublePrecision("quantity").notNull().default(0),
34187
- unit: text("unit"),
34188
- category: text("category"),
34189
- location: text("location"),
34190
- lowStockThreshold: doublePrecision("low_stock_threshold"),
34191
- expiresAt: timestamp("expires_at", { withTimezone: true }),
34192
- notes: text("notes"),
34193
- facet: jsonb("facet").$type(),
34194
- placeId: text("place_id").references(() => places.id, { onDelete: "set null" })
34195
- }, (t) => [
34196
- index("inventory_items_tenant_place_idx").on(t.tenantId, t.placeId),
34197
- index("inventory_items_tenant_idx").on(t.tenantId),
34198
- index("inventory_items_product_idx").on(t.productId)
34199
- ]);
34200
-
34201
34445
  // ../../packages/schema/src/presets/facets.ts
34202
34446
  var PANTRY_DISPLAY_KIND = "pantry";
34203
34447
  var MEDS_DISPLAY_KIND = "meds";
@@ -46298,6 +46542,221 @@ var commands2 = [
46298
46542
  return { data: { ok: true, animalId: id }, render: renderRecord };
46299
46543
  }
46300
46544
  },
46545
+ {
46546
+ spec: {
46547
+ path: ["animals", "medicines"],
46548
+ summary: "List the animals' medicines and where each stands today.",
46549
+ args: [],
46550
+ flags: [
46551
+ { name: "animal", type: "string", description: "Only this animal's (ULID)." },
46552
+ {
46553
+ name: "day",
46554
+ type: "string",
46555
+ description: "The day to read for, YYYY-MM-DD (default UTC today)."
46556
+ },
46557
+ { name: "include-ended", type: "boolean", description: "Ended courses too." }
46558
+ ],
46559
+ scopes: [...animalOperations.listMedicines.scopes],
46560
+ returns: "AnimalMedicineView[]",
46561
+ notes: `${routeOf(animalOperations.listMedicines)}. Each with its animals, dose, how often, the course's day N of M, the next day, the stock (low or not), and dueToday / doneToday.`
46562
+ },
46563
+ run: async ({ flags, requireClient }) => {
46564
+ const raw = {};
46565
+ const animal = flagString(flags, "animal");
46566
+ const day = flagString(flags, "day");
46567
+ if (animal)
46568
+ raw.animalId = animal;
46569
+ if (day)
46570
+ raw.day = day;
46571
+ if (flagBool(flags, "include-ended"))
46572
+ raw.includeEnded = "true";
46573
+ parseInput(animalMedicinesQuerySchema, raw);
46574
+ return {
46575
+ data: await requireClient().getPage(pathOf(animalOperations.listMedicines), { query: raw }),
46576
+ render: renderList
46577
+ };
46578
+ }
46579
+ },
46580
+ {
46581
+ spec: {
46582
+ path: ["animals", "medicine"],
46583
+ summary: "One medicine, as it stands today.",
46584
+ args: [{ name: "id", required: true, description: "Medicine ULID." }],
46585
+ flags: [],
46586
+ scopes: [...animalOperations.getMedicine.scopes],
46587
+ returns: "AnimalMedicineView",
46588
+ notes: `${routeOf(animalOperations.getMedicine)}.`
46589
+ },
46590
+ run: readOperation(animalOperations.getMedicine, renderRecord, { medicineId: "medicine id" })
46591
+ },
46592
+ {
46593
+ spec: {
46594
+ path: ["animals", "medicine-add"],
46595
+ summary: "Add a medicine for one animal or a few: daily, a course, or one that comes round.",
46596
+ args: [],
46597
+ flags: [
46598
+ {
46599
+ name: "data",
46600
+ type: "string",
46601
+ description: 'JSON: `{ "title": "Joint tablet", "animalIds": ["\u2026"], "dose": "1 tablet with breakfast", "timeOfDay": "morning", "stock": 22, "unit": "tablets", "lowAt": 5 }`; a course adds `startsOn`/`endsOn`, one that comes round `everyCount`/`everyUnit` (`day`, `week`, `month`) and `nextOn`. The id is minted.'
46602
+ }
46603
+ ],
46604
+ scopes: [...animalOperations.createMedicine.scopes],
46605
+ returns: "AnimalMedicineView",
46606
+ notes: `${routeOf(animalOperations.createMedicine)}. Daily unless said; each dose takes \`perDose\` (default 1) from a counted stock.`
46607
+ },
46608
+ run: async ({ flags, requireClient }) => {
46609
+ const body = parseInput(createAnimalMedicineInputSchema, {
46610
+ id: ulid3(),
46611
+ ...readDataFlag(flags)
46612
+ });
46613
+ return {
46614
+ data: await requireClient().post(pathOf(animalOperations.createMedicine), body),
46615
+ render: renderRecord
46616
+ };
46617
+ }
46618
+ },
46619
+ {
46620
+ spec: {
46621
+ path: ["animals", "medicine-update"],
46622
+ summary: "Change a medicine (a new box: set its stock).",
46623
+ args: [{ name: "id", required: true, description: "Medicine ULID." }],
46624
+ flags: [
46625
+ {
46626
+ name: "data",
46627
+ type: "string",
46628
+ description: "UpdateAnimalMedicineInput JSON; null clears a field."
46629
+ }
46630
+ ],
46631
+ scopes: [...animalOperations.updateMedicine.scopes],
46632
+ returns: "AnimalMedicineView",
46633
+ notes: `${routeOf(animalOperations.updateMedicine)}. \`animalIds\` replaces who it's for.`
46634
+ },
46635
+ run: async ({ args, flags, requireClient }) => {
46636
+ const medicineId = requireArg(args, 0, "medicine id");
46637
+ const body = parseInput(updateAnimalMedicineInputSchema, readDataFlag(flags));
46638
+ return {
46639
+ data: await requireClient().patch(pathOf(animalOperations.updateMedicine, { medicineId }), body),
46640
+ render: renderRecord
46641
+ };
46642
+ }
46643
+ },
46644
+ {
46645
+ spec: {
46646
+ path: ["animals", "medicine-remove"],
46647
+ summary: "Remove a medicine, with the doses given.",
46648
+ args: [{ name: "id", required: true, description: "Medicine ULID." }],
46649
+ flags: [],
46650
+ scopes: [...animalOperations.removeMedicine.scopes],
46651
+ returns: "void",
46652
+ notes: `${routeOf(animalOperations.removeMedicine)}. Destructive: the doses go too, so it needs --allow-destructive; to stop a course and keep it, set its endsOn.`
46653
+ },
46654
+ run: async ({ args, flags, requireClient }) => {
46655
+ const medicineId = requireArg(args, 0, "medicine id");
46656
+ assertDestructiveAllowed(flags, `remove medicine ${medicineId} and its doses`);
46657
+ await requireClient().delete(pathOf(animalOperations.removeMedicine, { medicineId }));
46658
+ return { data: { ok: true, medicineId }, render: renderRecord };
46659
+ }
46660
+ },
46661
+ {
46662
+ spec: {
46663
+ path: ["animals", "medicine-dose"],
46664
+ summary: "Give a medicine's dose (today, or --day).",
46665
+ args: [{ name: "id", required: true, description: "Medicine ULID." }],
46666
+ flags: [
46667
+ {
46668
+ name: "day",
46669
+ type: "string",
46670
+ description: "The day it's for, YYYY-MM-DD (default UTC today)."
46671
+ }
46672
+ ],
46673
+ scopes: [...animalOperations.giveMedicineDose.scopes],
46674
+ returns: "AnimalMedicineView",
46675
+ notes: `${routeOf(animalOperations.giveMedicineDose)}. Takes \`perDose\` from a counted stock and moves the next day on; again that day changes nothing.`
46676
+ },
46677
+ run: async ({ args, flags, requireClient }) => {
46678
+ const medicineId = requireArg(args, 0, "medicine id");
46679
+ const day = flagString(flags, "day");
46680
+ const body = parseInput(animalMedicineDoseInputSchema, day ? { day } : {});
46681
+ return {
46682
+ data: await requireClient().post(pathOf(animalOperations.giveMedicineDose, { medicineId }), body),
46683
+ render: renderRecord
46684
+ };
46685
+ }
46686
+ },
46687
+ {
46688
+ spec: {
46689
+ path: ["animals", "medicine-undose"],
46690
+ summary: "Take a medicine's dose back.",
46691
+ args: [
46692
+ { name: "id", required: true, description: "Medicine ULID." },
46693
+ { name: "day", required: true, description: "The day it was given for, YYYY-MM-DD." }
46694
+ ],
46695
+ flags: [],
46696
+ scopes: [...animalOperations.undoMedicineDose.scopes],
46697
+ returns: "AnimalMedicineView",
46698
+ notes: `${routeOf(animalOperations.undoMedicineDose)}. The stock and the next day go back to before it.`
46699
+ },
46700
+ run: async ({ args, requireClient }) => {
46701
+ const medicineId = requireArg(args, 0, "medicine id");
46702
+ const { day } = parseInput(animalMedicineDoseInputSchema, {
46703
+ day: requireArg(args, 1, "day")
46704
+ });
46705
+ return {
46706
+ data: await requireClient().delete(pathOf(animalOperations.undoMedicineDose, { medicineId, day })),
46707
+ render: renderRecord
46708
+ };
46709
+ }
46710
+ },
46711
+ {
46712
+ spec: {
46713
+ path: ["animals", "feeding-set"],
46714
+ summary: "Say how an animal is fed from a supply: what, how much, when, a note.",
46715
+ args: [
46716
+ { name: "id", required: true, description: "Animal ULID." },
46717
+ { name: "supplyId", required: true, description: "Supply (Household item) ULID." }
46718
+ ],
46719
+ flags: [
46720
+ {
46721
+ name: "data",
46722
+ type: "string",
46723
+ description: 'JSON: `{ "amount": "2 \xD7 110 g", "times": "07:30 and 18:00", "what": "\u2026", "note": "\u2026" }`; null clears a field.'
46724
+ }
46725
+ ],
46726
+ scopes: [...animalOperations.setFeeding.scopes],
46727
+ returns: "AnimalFeeding",
46728
+ notes: `${routeOf(animalOperations.setFeeding)}. Each field as written; one left out stays. Links the supply to the animal when it doesn't feed it yet. Read back as \`feedings\` on the animal's supplies.`
46729
+ },
46730
+ run: async ({ args, flags, requireClient }) => {
46731
+ const id = requireArg(args, 0, "animal id");
46732
+ const supplyId = requireArg(args, 1, "supply id");
46733
+ const body = parseInput(animalFeedingInputSchema, readDataFlag(flags));
46734
+ return {
46735
+ data: await requireClient().put(pathOf(animalOperations.setFeeding, { id, supplyId }), body),
46736
+ render: renderRecord
46737
+ };
46738
+ }
46739
+ },
46740
+ {
46741
+ spec: {
46742
+ path: ["animals", "feeding-off"],
46743
+ summary: "Take an animal's feeding off a supply (the supply still feeds it).",
46744
+ args: [
46745
+ { name: "id", required: true, description: "Animal ULID." },
46746
+ { name: "supplyId", required: true, description: "Supply (Household item) ULID." }
46747
+ ],
46748
+ flags: [],
46749
+ scopes: [...animalOperations.removeFeeding.scopes],
46750
+ returns: "void",
46751
+ notes: `${routeOf(animalOperations.removeFeeding)}. Only what was said about how it's fed goes; the link stays.`
46752
+ },
46753
+ run: async ({ args, requireClient }) => {
46754
+ const id = requireArg(args, 0, "animal id");
46755
+ const supplyId = requireArg(args, 1, "supply id");
46756
+ await requireClient().delete(pathOf(animalOperations.removeFeeding, { id, supplyId }));
46757
+ return { data: { ok: true, animalId: id, supplyId }, render: renderRecord };
46758
+ }
46759
+ },
46301
46760
  {
46302
46761
  spec: {
46303
46762
  path: ["animals", "acquisition"],
@@ -47522,12 +47981,12 @@ var commands9 = [
47522
47981
  {
47523
47982
  name: "type",
47524
47983
  type: "string",
47525
- description: "Type (book|film|game|music|place|wine|wardrobe|hobby)."
47984
+ description: "Type (book|film|game|music|place|wine|wardrobe|hobby), or a kind's key (`do collection kinds`)."
47526
47985
  },
47527
47986
  {
47528
47987
  name: "status",
47529
47988
  type: "string",
47530
- description: "Initial status; defaults to the Type's first."
47989
+ description: "Initial status; defaults to the Type's first (a kind's: want)."
47531
47990
  }
47532
47991
  ],
47533
47992
  scopes: [...collectionOperations.create.scopes],
@@ -47535,13 +47994,13 @@ var commands9 = [
47535
47994
  grammar: {
47536
47995
  "#tag": "Adds a tag. Repeatable: '#scifi #2024'."
47537
47996
  },
47538
- notes: '--type is required; the per-Type facet rides --data (validated server-side against the Type), e.g. `do collection add "Castle" --type hobby --status doing --data \'{"facet":{"kind":"LEGO","progress":{"done":7,"total":12,"unit":"bag"}}}\'`.'
47997
+ notes: `--type is required; the per-Type facet rides --data (validated server-side against the Type), e.g. \`do collection add "Castle" --type hobby --status doing --data '{"facet":{"kind":"LEGO","progress":{"done":7,"total":12,"unit":"bag"}}}'\`. An Item of one of the Space's own kinds takes its key as --type and answers its fields in the facet: \`do collection add "Harbour" --type vinyl --data '{"facet":{"artist":"Juniper"}}'\`.`
47539
47998
  },
47540
47999
  run: async ({ args, flags, requireClient }) => {
47541
48000
  const { title, tags } = parseTitleTags(args);
47542
48001
  if (title.length === 0)
47543
48002
  throw usageError('collection add needs a title, e.g. `do collection add "Dune" --type book`');
47544
- const type = requireFlag(flags, "type", "collection add needs --type <book|film|game|music|place|wine|wardrobe|hobby>");
48003
+ const type = requireFlag(flags, "type", "collection add needs --type <book|film|game|music|place|wine|wardrobe|hobby|a kind's key>");
47545
48004
  const body = parseInput(createCollectionItemInputSchema, {
47546
48005
  id: ulid3(),
47547
48006
  ...readDataFlag(flags),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robbeverhelst/do",
3
- "version": "1.392.0",
3
+ "version": "1.394.0",
4
4
  "private": false,
5
5
  "description": "do — the agent-drivable CLI over the public /api/v1 surface",
6
6
  "homepage": "https://github.com/robbeverhelst/do/tree/main/apps/cli",