mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
chore: update version to 0.2.11 and add developer notes for Logseq DB properties
This commit is contained in:
293
libs/development-notes/db_properties_notes.md
Normal file
293
libs/development-notes/db_properties_notes.md
Normal 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, 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<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
|
||||||
|
- 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<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`
|
||||||
|
|
||||||
406
libs/development-notes/db_properties_skill.md
Normal file
406
libs/development-notes/db_properties_skill.md
Normal 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)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@logseq/libs",
|
"name": "@logseq/libs",
|
||||||
"version": "0.2.10",
|
"version": "0.2.11",
|
||||||
"description": "Logseq SDK libraries",
|
"description": "Logseq SDK libraries",
|
||||||
"main": "dist/lsplugin.user.js",
|
"main": "dist/lsplugin.user.js",
|
||||||
"typings": "index.d.ts",
|
"typings": "index.d.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user