|
|
@@ -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)
|