chore: update version to 0.2.11 and add developer notes for Logseq DB properties

This commit is contained in:
charlie
2026-01-04 16:29:02 +08:00
parent 4b7d6c73c8
commit a2bfb39632
3 changed files with 700 additions and 1 deletions

View File

@@ -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, youre 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 youll 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<string, any> | 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
- Dont 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 youre 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<import('../src/LSPlugin').PropertySchema>,
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`

View File

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

View File

@@ -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",