@read-frog/api-contract 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@read-frog/api-contract",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@orpc/contract": "^1.16.1",
28
- "@read-frog/definitions": "0.13.0",
28
+ "@read-frog/definitions": "0.14.0",
29
29
  "zod": "^4.6.7"
30
30
  },
31
31
  "devDependencies": {
@@ -2,6 +2,8 @@ import { defineAgentProcedure } from "./shared";
2
2
  import { oc } from "@orpc/contract";
3
3
  import { pickPublicErrorMap } from "#/public-errors";
4
4
  import {
5
+ CardDeleteInputSchema,
6
+ CardDeleteOutputSchema,
5
7
  CardGenerateInputSchema,
6
8
  CardGenerateOutputSchema,
7
9
  CardGetInputSchema,
@@ -11,6 +13,20 @@ import {
11
13
  } from "#/schemas/card";
12
14
 
13
15
  export const cardContract = {
16
+ delete: oc
17
+ .errors(
18
+ pickPublicErrorMap(
19
+ "CARD_NOT_FOUND",
20
+ "NOTEBASE_NOT_FOUND",
21
+ "CARD_DELETE_UNSUPPORTED",
22
+ "RESOURCE_UPDATE_CONFLICT",
23
+ "CELL_VALIDATION_FAILED",
24
+ ),
25
+ )
26
+ .route({ method: "DELETE", path: "/cards/{id}", summary: "Delete card", tags: ["Cards"] })
27
+ .input(CardDeleteInputSchema)
28
+ .output(CardDeleteOutputSchema),
29
+
14
30
  list: defineAgentProcedure({
15
31
  method: "GET",
16
32
  path: "/cards",
@@ -92,7 +92,7 @@ export const notebaseRowContract = {
92
92
  path: "/notebase-rows/{notebaseRowId}",
93
93
  summary: "Update note",
94
94
  description:
95
- "Merge supplied fields into an owned note, preserving omitted fields. Use null to clear a field. expectedUpdatedAt guards against editing stale data; reread on a modification conflict.",
95
+ "Merge supplied fields into an owned note, preserving omitted fields. Use null to clear a field. expectedUpdatedAt guards against editing stale data; reread on a modification conflict. In a cloze field, a deletion's number ({{c1::…}}) is its card's identity: keep the numbers of the deletions you keep, and leave any gaps for the save to close. Renumbering them yourself hands a card's review history to other text, and can delete it.",
96
96
  tags: ["Notebase Rows"],
97
97
  })
98
98
  .errors(pickPublicErrorMap("NOTEBASE_ROW_NOT_FOUND", "CELL_VALIDATION_FAILED"))
@@ -105,7 +105,7 @@ export const notebaseRowContract = {
105
105
  path: "/notebase-rows/batch",
106
106
  summary: "Update notes",
107
107
  description:
108
- "Update 1–50 notes atomically in one owned notebase. Each row supplies its ID and fields to merge; omitted fields are preserved and null clears a field. Supply each note's expectedUpdatedAt to guard against stale edits. A missing row, invalid field or modification conflict rolls back the whole batch. Items are returned in input order.",
108
+ "Update 1–50 notes atomically in one owned notebase. Each row supplies its ID and fields to merge; omitted fields are preserved and null clears a field. Supply each note's expectedUpdatedAt to guard against stale edits. A missing row, invalid field or modification conflict rolls back the whole batch. Items are returned in input order. In a cloze field, a deletion's number ({{c1::…}}) is its card's identity: keep the numbers of the deletions you keep, and leave any gaps for the save to close. Renumbering them yourself hands a card's review history to other text, and can delete it.",
109
109
  tags: ["Notebase Rows"],
110
110
  })
111
111
  .errors(
@@ -21,6 +21,24 @@ export const CellValidationFailureReasonSchema = z.enum([
21
21
  "type_mismatch",
22
22
  "invalid_select_option",
23
23
  "invalid_date_format",
24
+ // A cloze deletion with a blank answer; receivedValue is the deletion as
25
+ // written (`{{c1::}}`).
26
+ "empty_cloze_answer",
27
+ // A cloze opening that never closes (unpaired braces, or no `}}`);
28
+ // receivedValue is the opening as written (`{{c1::`).
29
+ "unclosed_cloze",
30
+ // A cloze deletion whose ordinal list names no group (`{{c0::a}}`);
31
+ // receivedValue is the deletion as written.
32
+ "invalid_cloze_ordinal",
33
+ // A cloze deletion that cuts across a formula's delimiters (`\(a{{c1::b\) c}}`);
34
+ // receivedValue is the deletion as written.
35
+ "cloze_crosses_math",
36
+ // A cloze deletion in a formula that holds half of a `\left`/`\right` or
37
+ // `\begin`/`\end` pair; receivedValue is the deletion as written.
38
+ "cloze_splits_math_pair",
39
+ // More cloze groups in one note than CLOZE_MAX_GROUPS; receivedValue is
40
+ // the note's group count.
41
+ "too_many_cloze_groups",
24
42
  ]);
25
43
  export type CellValidationFailureReason = z.infer<typeof CellValidationFailureReasonSchema>;
26
44
 
@@ -42,6 +60,8 @@ export const CellValidationFailedDataSchema = z
42
60
  reason: CellValidationFailureReasonSchema,
43
61
  /** Index of the failing row within a batch input (createMany / updateMany / initialRows). */
44
62
  rowIndex: z.number().int().min(0).optional(),
63
+ /** The failing note, when the check ran over stored notes (a column type change). */
64
+ notebaseRowId: z.string().optional(),
45
65
  })
46
66
  .strict();
47
67
  export type CellValidationFailedData = z.infer<typeof CellValidationFailedDataSchema>;
@@ -103,6 +123,10 @@ export const SpeechDailyLimitReachedDataSchema = z.strictObject({
103
123
  });
104
124
 
105
125
  export const PUBLIC_APP_ERROR_DEFS = {
126
+ CARD_DELETE_UNSUPPORTED: {
127
+ message: "This card cannot be deleted independently of its note.",
128
+ status: 400,
129
+ },
106
130
  RESOURCE_UPDATE_CONFLICT: {
107
131
  message: "The resource changed. Read it again before updating.",
108
132
  status: 409,
@@ -114,6 +114,16 @@ export type CardGetInput = z.infer<typeof CardGetInputSchema>;
114
114
  export const CardGetOutputSchema = renderedCardSchema;
115
115
  export type CardGetOutput = z.infer<typeof CardGetOutputSchema>;
116
116
 
117
+ export const CardDeleteInputSchema = z.strictObject({
118
+ id: z.uuid(),
119
+ expectedVariantKey: z.string().min(1),
120
+ expectedRowUpdatedAt: expectedUpdatedAtSchema,
121
+ });
122
+ export type CardDeleteInput = z.infer<typeof CardDeleteInputSchema>;
123
+
124
+ export const CardDeleteOutputSchema = z.object({ txid: z.number() });
125
+ export type CardDeleteOutput = z.infer<typeof CardDeleteOutputSchema>;
126
+
117
127
  export const CardGenerateInputSchema = z.object({
118
128
  notebaseId: z.uuid(),
119
129
  templateId: z.uuid().optional(),
@@ -1,5 +1,10 @@
1
1
  import { paginationShape, pageSchema } from "./pagination";
2
2
  import { deletionResultSchema, expectedUpdatedAtSchema } from "./resource-mutation";
3
+ import {
4
+ noteCreatedViaSchema,
5
+ noteOriginInputSchema,
6
+ noteOriginSchema,
7
+ } from "@read-frog/definitions";
3
8
  import { z } from "zod";
4
9
 
5
10
  export const notebaseRowCellsSchema = z.record(z.string(), z.unknown());
@@ -9,15 +14,38 @@ export const notebaseRowSchema = z.object({
9
14
  notebaseId: z.string(),
10
15
  cells: notebaseRowCellsSchema,
11
16
  position: z.number(),
17
+ origin: noteOriginSchema
18
+ .nullish()
19
+ .describe(
20
+ "Where the note's content came from, set when it was created: a web page (url, title), a book on the user's shelf (ebookId, its title then, and cfi, the EPUB location of the selection), or a video (url, title, seconds into it). Null when it has none, or when it was created before this was recorded.",
21
+ ),
22
+ createdVia: noteCreatedViaSchema
23
+ .nullish()
24
+ .describe(
25
+ 'Which client created the note: "web", "extension", "mobile", "mcp" (with the MCP client\'s id and the name it registered under) or "unknown". Null for notes created before this was recorded.',
26
+ ),
12
27
  createdAt: z.coerce.date(),
13
28
  updatedAt: z.coerce.date(),
14
29
  });
15
30
  export type NotebaseRow = z.infer<typeof notebaseRowSchema>;
16
31
 
32
+ /**
33
+ * Where a new note's content came from; see `noteOriginInputSchema`. Optional
34
+ * metadata never costs the note: an origin that does not parse is dropped and
35
+ * the note is saved without one.
36
+ */
37
+ export const notebaseRowOriginInputSchema = noteOriginInputSchema
38
+ .optional()
39
+ .catch(undefined)
40
+ .describe(
41
+ "Where the note's content came from: a web page or video by its http(s) URL, or a book on the user's shelf by its id. Stored once and never edited. Optional: an origin that cannot be used is left out and the note is still saved.",
42
+ );
43
+
17
44
  export const notebaseRowCreateDataSchema = z
18
45
  .object({
19
46
  id: z.uuid().optional(),
20
47
  cells: notebaseRowCellsSchema.optional(),
48
+ origin: notebaseRowOriginInputSchema,
21
49
  })
22
50
  .strict();
23
51
  export type NotebaseRowCreateData = z.infer<typeof notebaseRowCreateDataSchema>;
@@ -88,6 +116,8 @@ export const NotebaseRowUpdateOutputSchema = z.object({
88
116
  notebaseId: z.uuid(),
89
117
  cells: notebaseRowCellsSchema,
90
118
  position: z.number().int(),
119
+ origin: notebaseRowSchema.shape.origin,
120
+ createdVia: notebaseRowSchema.shape.createdVia,
91
121
  createdAt: z.date(),
92
122
  updatedAt: z.date(),
93
123
  txid: z.number(),
@@ -8,7 +8,11 @@ import {
8
8
  } from "@read-frog/definitions";
9
9
  import { z } from "zod";
10
10
  import { notebaseColumnSchema } from "./notebase-column";
11
- import { NOTEBASE_ROW_CREATE_MANY_MAX_ROWS, notebaseRowSchema } from "./notebase-row";
11
+ import {
12
+ NOTEBASE_ROW_CREATE_MANY_MAX_ROWS,
13
+ notebaseRowOriginInputSchema,
14
+ notebaseRowSchema,
15
+ } from "./notebase-row";
12
16
  import { notebaseViewSchema } from "./notebase-view";
13
17
 
14
18
  export const NotebaseListInputSchema = z.object({});
@@ -55,6 +59,7 @@ const notebaseInitialRowSchema = z
55
59
  .object({
56
60
  id: z.uuid().optional(),
57
61
  cells: z.record(z.string(), z.unknown()),
62
+ origin: notebaseRowOriginInputSchema,
58
63
  })
59
64
  .strict();
60
65
 
@@ -26,6 +26,7 @@ export const webSyncOperationSchema = z.enum([
26
26
  "bury",
27
27
  "suspend",
28
28
  "deleteNote",
29
+ "deleteCard",
29
30
  // A synced collection's own mutation handlers.
30
31
  "insert",
31
32
  "update",