From a2bfb39632291308a2e156b738f483301fd42e16 Mon Sep 17 00:00:00 2001 From: charlie Date: Sun, 4 Jan 2026 16:29:02 +0800 Subject: [PATCH] chore: update version to 0.2.11 and add developer notes for Logseq DB properties --- libs/development-notes/db_properties_notes.md | 293 +++++++++++++ libs/development-notes/db_properties_skill.md | 406 ++++++++++++++++++ libs/package.json | 2 +- 3 files changed, 700 insertions(+), 1 deletion(-) create mode 100644 libs/development-notes/db_properties_notes.md create mode 100644 libs/development-notes/db_properties_skill.md diff --git a/libs/development-notes/db_properties_notes.md b/libs/development-notes/db_properties_notes.md new file mode 100644 index 0000000000..cb45e6ec31 --- /dev/null +++ b/libs/development-notes/db_properties_notes.md @@ -0,0 +1,293 @@ +# Logseq DB Properties (Developer Notes) + +This note explains how **properties** work in Logseq when using a **DB graph** (database mode), how that differs from a **file graph** (Markdown/Org), and how to use the **plugin SDK** APIs defined in `libs/src/LSPlugin.ts`. + +> Scope: This is written for plugin developers. It focuses on `logseq.Editor.*` property APIs and tag/class modeling. + +--- + +## 1) Two worlds: File graph vs DB graph + +### File graph (Markdown / Org) +- “Properties” are primarily **text syntax** stored in the file: + - Markdown frontmatter (`---`) + - Org property drawers (`:PROPERTIES:`) + - Inline props (`key:: value`) +- When you update a property, you’re often ultimately **editing text**. +- SDK calls like `Editor.insertBlock`/`updateBlock` have `opts.properties`, but note this warning in the typings: + - `IBatchBlock.properties` is **not supported for DB graph**. + +### DB graph (Database mode) +DB mode treats properties as **first-class DB entities**. + +Think of it as a 2-layer model: + +1) **Property definitions (schema)** + - A property key (e.g. `rating`, `authors`, `zotero_key`) exists as an entity. + - It may have a schema: type, cardinality, visibility. + +2) **Property values (data)** + - Blocks/pages can store values for property keys. + - Persisted structurally in the DB (not by emitting `key:: value` text). + +In DB graphs, prefer the dedicated APIs: +- Property schema: `Editor.getProperty / upsertProperty / removeProperty` +- Values on blocks: `Editor.upsertBlockProperty / removeBlockProperty / getBlockProperties` + +--- + +## 2) Important types (from `LSPlugin.ts`) + +### `PropertySchema` +```ts +export type PropertySchema = { + type: 'default' | 'number' | 'node' | 'date' | 'checkbox' | 'url' | string, + cardinality: 'many' | 'one', + hide: boolean + public: boolean +} +``` + +Practical meaning: +- `type` + - Controls editor/UI behavior and (in DB graphs) how values are interpreted. + - Common: `default`, `number`, `date`, `checkbox`, `url`, `node`. +- `cardinality` + - `'one'`: a single value + - `'many'`: multiple values (typically passed as an array) +- `hide` + - Hide in UI property panels. +- `public` + - Expose property in UI (and typically configure discoverability). + +### Entities you’ll see +- `BlockEntity`: blocks, but also used for some “special blocks” such as property entities. +- `PageEntity`: pages, tags (classes), property pages (`type: 'property'`), etc. +- `BlockIdentity`: a block uuid string OR `{ uuid }`. + +--- + +## 3) DB-only vs graph-agnostic checks + +Before doing DB-only work, check graph type: + +```ts +const isDbGraph = await logseq.App.checkCurrentIsDbGraph() +if (!isDbGraph) { + await logseq.UI.showMsg('This feature requires a DB graph.', 'warning') + return +} + +// Also check app-level capability: +const { supportDb } = await logseq.App.getInfo() +``` + +--- + +## 4) Property schema APIs (DB only) + +### Get a property definition +```ts +const propEntity = await logseq.Editor.getProperty('zotero_key') +// -> BlockEntity | null +``` + +### Create or update a property definition (idempotent) +Use `upsertProperty` to ensure the property exists and has the schema you expect. + +```ts +await logseq.Editor.upsertProperty( + 'zotero_key', + { + type: 'default', + cardinality: 'one', + hide: false, + public: true, + }, + { name: 'Zotero Key' } +) +``` + +Notes: +- `key` is your stable identifier (recommend `snake_case`). +- `opts.name` can be used as a **display name** for users. + +### Remove a property definition +```ts +await logseq.Editor.removeProperty('zotero_key') +``` + +--- + +## 5) Block/page property value APIs + +### Set (upsert) a property value on a block +```ts +const block = await logseq.Editor.getCurrentBlock() +if (!block) return + +await logseq.Editor.upsertBlockProperty(block.uuid, 'zotero_key', 'ABCD1234') +``` + +### Set multi-value properties (`cardinality: 'many'`) +For `many`, pass an array. Use `{ reset: true }` when you want to overwrite vs merge. + +```ts +await logseq.Editor.upsertBlockProperty( + block.uuid, + 'authors', + ['Ada Lovelace', 'Alan Turing'], + { reset: true } +) +``` + +### Remove a value +```ts +await logseq.Editor.removeBlockProperty(block.uuid, 'zotero_key') +``` + +### Read properties +```ts +const props = await logseq.Editor.getBlockProperties(block.uuid) +// -> Record | null + +const pageProps = await logseq.Editor.getPageProperties('My Page') +``` + +### Read a single property (returns a `BlockEntity | null`) +`getBlockProperty` is useful when you want the DB entity wrapper for a key. + +```ts +const v = await logseq.Editor.getBlockProperty(block.uuid, 'zotero_key') +``` + +--- + +## 6) Tags as “classes” + attaching properties (DB modeling) + +In DB graphs, tags can behave like **classes**. + +Relevant APIs: +- `Editor.createTag(tagName, opts)` +- `Editor.addTagProperty(tagId, propertyIdOrName)` / `removeTagProperty` +- `Editor.addTagExtends(tagId, parentTagIdOrName)` / `removeTagExtends` +- `Editor.addBlockTag(blockId, tagId)` / `removeBlockTag` + +### Create a tag with tagProperties +This is the most developer-friendly way to ship a “schema bundle”: + +```ts +const tag = await logseq.Editor.createTag('ZoteroItem', { + tagProperties: [ + { + name: 'zotero_key', + schema: { type: 'default', cardinality: 'one', public: true, hide: false }, + }, + { + name: 'authors', + schema: { type: 'node', cardinality: 'many', public: true, hide: false }, + }, + { + name: 'published_at', + schema: { type: 'date', cardinality: 'one', public: true, hide: false }, + }, + ], +}) + +if (tag) { + const block = await logseq.Editor.getCurrentBlock() + if (block) await logseq.Editor.addBlockTag(block.uuid, tag.uuid) +} +``` + +Why this pattern is popular: +- Users see a consistent “record type” (`ZoteroItem`) with fields. +- Plugins can be idempotent: create if missing, otherwise just reuse. + +### Add properties to an existing tag later +```ts +const tag = await logseq.Editor.getTag('ZoteroItem') +if (tag) { + await logseq.Editor.addTagProperty(tag.uuid, 'zotero_key') + await logseq.Editor.addTagProperty(tag.uuid, 'authors') +} +``` + +### Tag inheritance +```ts +await logseq.Editor.addTagExtends('ZoteroItem', 'Reference') +``` + +--- + +## 7) Recommended conventions for plugin authors + +### Prefer DB APIs over text editing in DB graphs +- Don’t generate `key:: value` strings as your “source of truth” in DB mode. +- Use `upsertProperty` + `upsertBlockProperty`. + +### Keep keys stable; use display names for UX +- Use safe keys (`snake_case`, ASCII) for `key`. +- Use `upsertProperty(key, schema, { name })` to show a nice label. + +### Be explicit about cardinality +- If you intend a list, set `cardinality: 'many'` and always write arrays. +- Use `{ reset: true }` when you want to overwrite. + +### Guard DB-only APIs +- Always check `App.checkCurrentIsDbGraph()`. + +--- + +## 8) A tiny helper wrapper (optional) + +If you’re building a small internal “DB properties SDK” for your plugin, a minimal shape looks like: + +```ts +export async function ensureDbGraph() { + if (!(await logseq.App.checkCurrentIsDbGraph())) { + throw new Error('DB graph required') + } +} + +export async function ensureProperty( + key: string, + schema: Partial, + name?: string +) { + return logseq.Editor.upsertProperty(key, schema, name ? { name } : undefined) +} + +export async function setProp( + block: string, + key: string, + value: any, + reset = false +) { + return logseq.Editor.upsertBlockProperty(block, key, value, reset ? { reset: true } : undefined) +} +``` + +--- + +## Appendix: API quick reference (from `LSPlugin.ts`) + +**Property schema (DB only)** +- `Editor.getProperty(key)` +- `Editor.upsertProperty(key, schema?, opts?)` +- `Editor.removeProperty(key)` + +**Block/page values** +- `Editor.upsertBlockProperty(block, key, value, { reset? })` +- `Editor.removeBlockProperty(block, key)` +- `Editor.getBlockProperty(block, key)` +- `Editor.getBlockProperties(block)` +- `Editor.getPageProperties(page)` + +**Tags / Classes** +- `Editor.createTag(tagName, { tagProperties? })` +- `Editor.getTag(nameOrIdent)` / `Editor.getAllTags()` +- `Editor.addTagProperty(tagId, propertyIdOrName)` / `removeTagProperty` +- `Editor.addTagExtends(tagId, parentTagIdOrName)` / `removeTagExtends` +- `Editor.addBlockTag(blockId, tagId)` / `removeBlockTag` + diff --git a/libs/development-notes/db_properties_skill.md b/libs/development-notes/db_properties_skill.md new file mode 100644 index 0000000000..723ae5e36e --- /dev/null +++ b/libs/development-notes/db_properties_skill.md @@ -0,0 +1,406 @@ +# Logseq DB Properties SDK Reference + +## Overview + +Logseq has two storage modes: +- **File-based**: Properties stored as `key:: value` in markdown files +- **DB-based**: Properties stored in SQLite with structured schema, types, and relationships + +This guide covers **DB-based properties API** for plugin development. + +--- + +## Core Concepts + +### 1. Property Schema (Global Definition) +Properties must be defined globally before use. Think of it as creating columns in a database. + +### 2. Tags as Classes +Tags act as classes/types. Pages/blocks can be tagged to inherit property schemas. + +### 3. Property Values +After defining properties, assign values to specific blocks/pages. + +--- + +## API Reference + +### Property Schema APIs + +#### `upsertProperty(name, options)` +Define or update a property schema globally. + +**Parameters:** +- `name` (string): Property name +- `options` (object): + - `type`: `'default'` | `'node'` | `'date'` | `'number'` + - `cardinality`: `'one'` | `'many'` (for `node` type) + +**Returns:** Property object with `uuid` + +**Example:** +```typescript +// Simple text property +await logseq.Editor.upsertProperty('author', { type: 'default' }) + +// Reference property (links to other pages) +await logseq.Editor.upsertProperty('tags', { + type: 'node', + cardinality: 'many' +}) + +// Date property +await logseq.Editor.upsertProperty('publishedDate', { type: 'date' }) +``` + +--- + +### Tag (Class) Management APIs + +#### `createTag(name, options? )` +Create a tag as a class/type definition. + +**Parameters:** +- `name` (string): Tag name +- `options` (optional object): `{ uuid: string }` + +**Returns:** Tag object with `uuid` and `id` + +**Example:** +```typescript +const bookTag = await logseq.Editor.createTag('book') +const articleTag = await logseq.Editor.createTag('article', { + uuid: 'custom-uuid-123' +}) +``` + +#### `getTag(name)` +Retrieve an existing tag by name. + +**Returns:** Tag object or `null` + +**Example:** +```typescript +const tag = await logseq.Editor. getTag('book') +if (! tag) { + // Tag doesn't exist, create it +} +``` + +#### `addTagProperty(tagUUID, propertyName)` +Add a property to a tag's schema. Pages tagged with this tag will have this property available. + +**Parameters:** +- `tagUUID` (string): Tag UUID +- `propertyName` (string): Property name (must be defined via `upsertProperty` first) + +**Example:** +```typescript +// Define properties globally +await logseq.Editor. upsertProperty('author', { type: 'default' }) +await logseq.Editor.upsertProperty('isbn', { type: 'default' }) + +// Add properties to 'book' tag +const bookTag = await logseq. Editor.createTag('book') +await logseq.Editor.addTagProperty(bookTag.uuid, 'author') +await logseq.Editor.addTagProperty(bookTag.uuid, 'isbn') +``` + +--- + +### Block/Page Property Value APIs + +#### `upsertBlockProperty(blockUUID, propertyName, value)` +Set property value for a specific block or page. + +**Parameters:** +- `blockUUID` (string): Block or page UUID +- `propertyName` (string): Property name +- `value` (any): Property value + - For `node` type with `cardinality: 'many'`: Use array of page IDs + - For `node` type with `cardinality: 'one'`: Use single page ID + - For other types: Use primitive values + +**Example:** +```typescript +const page = await logseq.Editor.getPage('my-page-uuid') + +// Set simple value +await logseq.Editor.upsertBlockProperty(page.uuid, 'author', 'John Doe') + +// Set number +await logseq.Editor.upsertBlockProperty(page.uuid, 'year', 2024) + +// Set single reference +await logseq.Editor. upsertBlockProperty(page.uuid, 'category', categoryPageID) + +// Set multiple references +await logseq.Editor.upsertBlockProperty( + page.uuid, + 'tags', + [tagPageID1, tagPageID2, tagPageID3] +) +``` + +#### `addBlockTag(blockUUID, tagUUID)` +Tag a block or page, making it an instance of that tag's class. + +**Parameters:** +- `blockUUID` (string): Block or page UUID +- `tagUUID` (string): Tag UUID + +**Example:** +```typescript +const page = await logseq. Editor.createPage('Moby Dick') +const bookTag = await logseq.Editor.getTag('book') +await logseq.Editor.addBlockTag(page.uuid, bookTag.uuid) +``` + +--- + +### Tag Inheritance + +#### Set Tag Parent (Class Inheritance) +Invoke API of `logseq.Editor.setTagExtends` + +**Example:** +```typescript +// Create parent tag +const rootTag = await logseq.Editor.createTag('Media') + +// Create child tag +const bookTag = await logseq.Editor.createTag('book') + +// Set inheritance +await logseq.Editor.addTagExtends(bookTag.uuid, rootTag.uuid) + +// Use special property `:logseq.property.class/extends` to create tag hierarchy. +// await logseq.Editor. upsertBlockProperty( +// bookTag.uuid, +// ':logseq.property.class/extends', +// [rootTag.id] // Use . id, not .uuid +// ) +``` + +--- + +### Special Properties + +#### Hide Empty Values +```typescript +const prop = await logseq.Editor. upsertProperty('optionalField', { type: 'default' }) +await logseq.Editor.upsertBlockProperty( + prop.uuid, + ': logseq.property/hide-empty-value', + true +) +``` + +--- + +## Common Patterns + +### Pattern 1: Define a Schema with Class Hierarchy + +```typescript +async function setupBookSchema() { + // 1. Create root class + const mediaTag = await logseq.Editor.createTag('Media') + + // 2. Define common properties + await logseq. Editor.upsertProperty('title', { type: 'default' }) + await logseq.Editor.upsertProperty('year', { type: 'number' }) + await logseq.Editor.upsertProperty('tags', { type: 'node', cardinality: 'many' }) + + // 3. Add properties to root class + await logseq. Editor.addTagProperty(mediaTag.uuid, 'title') + await logseq.Editor.addTagProperty(mediaTag.uuid, 'year') + await logseq.Editor. addTagProperty(mediaTag.uuid, 'tags') + + // 4. Create subclass + const bookTag = await logseq.Editor.createTag('book') + await logseq.Editor.upsertBlockProperty( + bookTag.uuid, + ':logseq.property.class/extends', + [mediaTag.id] + ) + + // 5. Add book-specific properties + await logseq.Editor.upsertProperty('author', { type: 'default' }) + await logseq.Editor.upsertProperty('isbn', { type: 'default' }) + await logseq.Editor.addTagProperty(bookTag.uuid, 'author') + await logseq.Editor.addTagProperty(bookTag.uuid, 'isbn') +} +``` + +### Pattern 2: Create an Instance + +```typescript +async function createBookInstance(bookData) { + // 1. Create page with stable UUID + const pageUUID = generateStableUUID(bookData.id) + let page = await logseq.Editor.getPage(pageUUID) + + if (!page) { + page = await logseq.Editor.createPage( + bookData.title, + {}, + { customUUID: pageUUID, redirect: false } + ) + } + + // 2. Tag the page + const bookTag = await logseq.Editor.getTag('book') + await logseq.Editor.addBlockTag(page.uuid, bookTag. uuid) + + // 3. Set property values + await logseq. Editor.upsertBlockProperty(page.uuid, 'title', bookData.title) + await logseq.Editor.upsertBlockProperty(page.uuid, 'author', bookData.author) + await logseq.Editor.upsertBlockProperty(page.uuid, 'isbn', bookData.isbn) + await logseq.Editor.upsertBlockProperty(page.uuid, 'year', bookData.year) + + return page. uuid +} +``` + +### Pattern 3: Link Related Entities + +```typescript +async function linkBookToCategories(bookPageUUID, categoryKeys) { + // Get or create category pages + const categoryIDs = await Promise.all( + categoryKeys.map(async (key) => { + const catUUID = generateStableUUID(key) + let catPage = await logseq.Editor.getPage(catUUID) + + if (!catPage) { + catPage = await logseq.Editor.createPage(key, {}, { + customUUID: catUUID + }) + } + + return catPage.id // Use . id for references + }) + ) + + // Link book to categories + await logseq.Editor.upsertBlockProperty( + bookPageUUID, + 'tags', // Must be 'node' type with 'many' cardinality + categoryIDs + ) +} +``` + +### Pattern 4: Batch Schema Setup from JSON + +```typescript +async function setupSchemaFromConfig(config) { + // config = { "book": { "fields": ["author", "isbn"], "parent": "Media" } } + + for (const [tagName, tagConfig] of Object.entries(config)) { + let tag = await logseq.Editor.getTag(tagName) + + if (!tag) { + tag = await logseq.Editor.createTag(tagName) + + // Set parent if specified + if (tagConfig. parent) { + const parentTag = await logseq. Editor.getTag(tagConfig. parent) + if (parentTag) { + await logseq.Editor.upsertBlockProperty( + tag. uuid, + ':logseq.property.class/extends', + [parentTag.id] + ) + } + } + } + + // Add properties + for (const fieldName of tagConfig.fields) { + await logseq.Editor.upsertProperty(fieldName, { type: 'default' }) + await logseq.Editor.addTagProperty(tag.uuid, fieldName) + } + } +} +``` + +--- + +## Best Practices + +### 1. Use Stable UUIDs +Generate deterministic UUIDs from stable identifiers: + +```typescript +import { v5 as uuidv5 } from 'uuid' + +const NAMESPACE = 'your-plugin-namespace-uuid' + +function generateStableUUID(id: string): string { + return uuidv5(id, NAMESPACE) +} +``` + +### 2. Check Before Creating +Always check if tags/pages exist before creating: + +```typescript +let tag = await logseq.Editor.getTag('book') +if (!tag) { + tag = await logseq.Editor.createTag('book') +} +``` + +### 3. Property Definition Order +1. Define property globally with `upsertProperty` +2. Create tag with `createTag` +3. Add property to tag with `addTagProperty` +4. Create page and tag it with `addBlockTag` +5. Set values with `upsertBlockProperty` + +### 4. Use . id for References +When setting `node` type properties, use `.id` not `.uuid`: + +```typescript +await logseq.Editor.upsertBlockProperty( + page. uuid, + 'category', + categoryPage.id // ✅ Correct + // categoryPage.uuid // ❌ Wrong +) +``` + +### 5. Error Handling +Wrap API calls in try-catch for robustness: + +```typescript +try { + await logseq.Editor.upsertBlockProperty(uuid, 'field', value) +} catch (error) { + console.error(`Failed to set property: ${error}`) + // Handle gracefully +} +``` + +--- + +## Real-World Example: Zotero Plugin + +See complete implementation: [xyhp915/logseq-zotero-plugin](https://github.com/xyhp915/logseq-zotero-plugin/blob/main/src/handlers.ts#L19-L207) + +Key techniques: +- Schema defined from `z_item_types. json` metadata +- Tag hierarchy: `Zotero` → `book`, `article`, etc. +- Stable UUIDs via `v5(itemKey, namespace)` +- Relationship properties for collections +- Nested blocks for attachments + +--- + +## Reference + +- **Type Definitions**: See `@logseq/libs` package +- **Examples**: [logseq-zotero-plugin/handlers.ts](https://github.com/xyhp915/logseq-zotero-plugin/blob/main/src/handlers. ts) +- **Search More**: [GitHub Code Search](https://github.com/search?type=code&q=repo:xyhp915/logseq-zotero-plugin+upsertBlockProperty) \ No newline at end of file diff --git a/libs/package.json b/libs/package.json index bc8c1873d4..adb2a55ef3 100644 --- a/libs/package.json +++ b/libs/package.json @@ -1,6 +1,6 @@ { "name": "@logseq/libs", - "version": "0.2.10", + "version": "0.2.11", "description": "Logseq SDK libraries", "main": "dist/lsplugin.user.js", "typings": "index.d.ts",